Compare commits

..

284 Commits

Author SHA1 Message Date
Ferdinand Schober
4703a4c947 chore: Release 2024-11-07 12:49:59 +01:00
Ferdinand Schober
a870a9e3a9 split features for emulation and capture backends 2024-11-07 12:43:42 +01:00
Ferdinand Schober
1433a3021b fix conditional compilation for xdp only build 2024-11-07 12:21:38 +01:00
Ferdinand Schober
ef92bd69d9 Update README.md 2024-11-07 02:37:40 +01:00
Ferdinand Schober
526d8004b6 Update README.md 2024-11-07 02:36:11 +01:00
Ferdinand Schober
fef702ffdd Update README.md (#229) 2024-11-07 02:35:32 +01:00
Ferdinand Schober
41ab25cc19 update screenshots (#228) 2024-11-07 01:18:55 +01:00
Ferdinand Schober
66456f18f1 update core-graphics / foundation (#227) 2024-11-07 01:15:35 +01:00
Ferdinand Schober
1d25dfbe50 upgrade ashpd + reis (#226) 2024-11-07 00:38:26 +01:00
Ferdinand Schober
9d28fe6c7b bump dependencies 2024-11-06 23:53:42 +01:00
byquanton
de674c631a Rename Synergy Community Edition to Deskflow (#225) 2024-11-06 13:46:19 +01:00
Ferdinand Schober
cd2cabf25b adapt config backend options to cli arg names 2024-11-06 12:14:47 +01:00
Ferdinand Schober
71f7e2e5e0 fix left over capture barrier 2024-11-05 14:32:39 +01:00
Ferdinand Schober
e7a18b9696 recreate wl_{pointer,keyboard} on capability event (#222)
this should fix #88
fixes #123
2024-11-04 22:45:57 +01:00
Ferdinand Schober
7496015d8d macos: implement client side modifier events (#219)
closes #198
closes #199
2024-10-27 08:41:26 +01:00
Ferdinand Schober
75b790ec2e propagate event tap creation error (#218) 2024-10-25 16:31:15 +02:00
Emile Akbarzadeh
9f52a2ac93 Add default.nix file to main and update readme (#211)
Co-authored-by: Emile Akbarzadeh <emile@dromeda.com.au>
2024-10-25 16:20:15 +02:00
Ferdinand Schober
4856199153 fix windows ci 2024-10-25 15:12:20 +02:00
虢豳
24e7a1d07f Add desktop and icon (#212)
* nix flake update

* nix: add desktop and icon

closes #210
2024-10-07 12:05:07 +02:00
Ferdinand Schober
555fbfeb79 formatting 2024-10-05 21:48:49 +02:00
Ferdinand Schober
6191216873 windows: fix panic when recreating input-capture
RegisterWindowClassW fails when called again
2024-10-05 21:47:12 +02:00
Ferdinand Schober
5b1dc4ccf8 reference count capture (#209)
* reference count capture

Multiple captures can now be created at the same position.
Captures at the same position are reference counted.

* update testcase

will be required by #200 / #164
2024-10-05 21:22:28 +02:00
Ferdinand Schober
ab1b45ff45 macos: fix key-release with repeat logic (#206) 2024-10-04 18:21:40 +02:00
Jacob Barber
b071201dcb Fix multimonitors (#202)
Co-authored-by: Jacob Barber <jacob.barber@disney.com>

closes #83
2024-09-20 20:50:37 +02:00
Nick Bolton
f52f19d2e3 Add link to Synergy (open source) (#194) 2024-09-10 19:06:09 +02:00
Ferdinand Schober
39fed0344c cleanup server code + fix a lost update case (#191) 2024-09-05 02:31:10 +02:00
Ferdinand Schober
6cd190191e cleanup main (#189) 2024-09-05 01:06:55 +02:00
Ferdinand Schober
be677d4c81 extract frontend crate (#186) 2024-09-04 17:29:29 +02:00
Ferdinand Schober
12bc0d86ca layer-shell: drop hard-dep on shortcut-inhibit (#188)
soften dependencies off layer-shell backend to make
https://wayland.app/protocols/keyboard-shortcuts-inhibit-unstable-v1
optional.

This allows partial functionality on compositors that don't support
the protocol, e.g. labwc.
2024-09-03 23:24:14 +02:00
Ferdinand Schober
1f7a7309eb include commit-hash in version (#185) 2024-09-02 19:46:07 +02:00
Ferdinand Schober
8926d8f803 produce events in dummy capture-backend (#184) 2024-09-02 17:59:17 +02:00
Johan
9248007986 [WIP] MacOS inputcapture (#131)
* [WIP] MacOS inputcapture

---------

Co-authored-by: Ferdinand Schober <ferdinand.schober@fau.de>
Co-authored-by: Ferdinand Schober <ferdinandschober20@gmail.com>
2024-08-26 12:40:45 +02:00
Ferdinand Schober
e7a1d72149 use local-channel instead of tokio sync channel (#179)
this avoids the mutex overhead in tokio
2024-08-12 18:20:21 +02:00
Ferdinand Schober
19c2c4327f move lan-mouse protocol to separate crate (#178) 2024-08-11 16:51:47 +02:00
Ferdinand Schober
fe06ca1fae cleanup capture task (#177)
* cleanup capture task

* rename {Capture,Emulation}Event to %Request
2024-08-09 14:43:55 +02:00
Ferdinand Schober
266ad28c6b track pressed keys in input-capture (#170)
move pressed key tracking to input capture
2024-08-09 13:18:23 +02:00
Ferdinand Schober
096567640c fix pressed key tracking
accidentally broke this in 8f7890c

closes #174
2024-08-08 13:33:04 +02:00
Ferdinand Schober
8f7890c9be move refcounting of key presses to input-emulation (#169) 2024-08-06 16:46:32 +02:00
Ferdinand Schober
68361b25d1 fix crash due to dropped fd (#167) 2024-08-05 14:16:45 +02:00
Ferdinand Schober
22dc33367b chore: Release 2024-07-30 11:13:05 +02:00
Ferdinand Schober
ec412a5e74 chore: Release 2024-07-30 11:12:20 +02:00
Ferdinand Schober
e1096ae86c fix macos build error 2024-07-30 11:11:45 +02:00
Ferdinand Schober
de3167221c chore: Release 2024-07-30 11:07:49 +02:00
Ferdinand Schober
0509b51a61 chore: Release 2024-07-30 11:06:27 +02:00
Ferdinand Schober
281cb406dd chore: Release 2024-07-30 11:06:04 +02:00
Ferdinand Schober
06ac390dbf chore: Release 2024-07-30 11:05:03 +02:00
Ferdinand Schober
dcc9250b6d fix repository url 2024-07-30 11:04:21 +02:00
Ferdinand Schober
376ae50b45 chore: Release 2024-07-30 11:03:08 +02:00
Ferdinand Schober
0e2c749b29 fix conditional compilation 2024-07-30 10:52:56 +02:00
Ferdinand Schober
127c3366bf derive barrier_id from cursor position (#162)
this should fix #140
2024-07-19 15:23:04 +02:00
Ferdinand Schober
00e1ded35d fix windows build 2024-07-19 15:06:13 +02:00
Ferdinand Schober
65fb228db5 upgrade dependencies 2024-07-19 12:58:52 +02:00
Ferdinand Schober
975d4b58a5 fix scrolling factor in xdp emulation 2024-07-17 11:26:43 +02:00
Ferdinand Schober
0be85f63f7 ensure all keys are released when emulation ends 2024-07-16 22:43:56 +02:00
Ferdinand Schober
bea7d6f8a5 Allow input capture & emulation being disabled (#158)
* Input capture and emulation can now be disabled and will prompt the user to enable again.

* Improved error handling to deliver more useful error messages
2024-07-16 20:34:46 +02:00
Hannes Schulze
55bdf1e63e Update Nix Flake (#161)
* nix: add support for aarch64-linux

* nix: add librsvg to enable svg icons in gtk
2024-07-16 14:59:36 +02:00
Bill Doyle
84696760f0 Add warning about Windows hiding the cursor (#160) 2024-07-13 00:55:02 +02:00
Ferdinand Schober
6a4dd740c3 code cleanup + purge anyhow in library code (#157) 2024-07-10 00:33:49 +02:00
Ferdinand Schober
703465a370 fix drop impl for desktop-portal 2024-07-05 01:41:11 +02:00
Ferdinand Schober
ef3ebc59bd adjust error handling 2024-07-05 01:41:11 +02:00
Ferdinand Schober
37a8f729ea remove dispatch workaround 2024-07-05 01:41:11 +02:00
Ferdinand Schober
9abec63313 update dependencies 2024-07-03 11:11:50 +02:00
Ferdinand Schober
35e626976e add explicit version 2024-07-02 22:39:20 +02:00
Ferdinand Schober
684735b499 fix dependencies in input-event 2024-07-02 22:34:43 +02:00
Ferdinand Schober
abfc744e4c specify dependency versions explicitly 2024-07-02 22:17:26 +02:00
Ferdinand Schober
fb2c39e8ae fix all features enabled by default 2024-07-02 22:07:37 +02:00
Ferdinand Schober
82ab5ecbbd fix xdg-desktop-portal backend not available 2024-07-02 22:07:37 +02:00
Ferdinand Schober
3fd2b31562 update deps 2024-07-02 22:07:37 +02:00
Ferdinand Schober
90e83cee87 purge dependencies 2024-07-02 22:07:37 +02:00
Ferdinand Schober
4db2d37f32 split into input-{event,capture,emulation} 2024-07-02 22:07:37 +02:00
Ferdinand Schober
7b511bb97d fix iteration order 2024-07-02 16:23:07 +02:00
Ferdinand Schober
70a23b9fa7 reduce coupling of emulation and capture backends 2024-07-02 16:23:07 +02:00
Ferdinand Schober
b6b16063a8 Configurable emulation backend (#151) 2024-07-01 20:09:16 +02:00
Ferdinand Schober
9cbe1ed8d8 fix release bind message 2024-06-30 14:41:16 +02:00
Ferdinand Schober
3528ef4fae Configurable capture backend (#150)
capture backend can now be configured via the `capture_backend` cli argument / config entry
2024-06-29 00:10:36 +02:00
Ferdinand Schober
232c048c19 fix transmuting to pointer types UB (#147)
closes #134
2024-06-25 14:13:05 +02:00
Orhun Parmaksız
1c37579ae5 Update Arch Linux instructions (#145) 2024-06-19 20:28:30 +02:00
Ferdinand Schober
460bacade5 fix sizeof usize assumed to be 8 (#143)
closes #141
2024-06-09 00:49:00 +02:00
虢豳
5fd3b719d6 Extract package name and version from Cargo.toml (#136)
* chore: nix flake update

* feat: Extract package name and version from Cargo.toml
2024-05-22 08:06:44 +02:00
Ferdinand Schober
e6d4585bb2 chore: Release lan-mouse version 0.8.0 2024-05-17 18:06:11 +02:00
Ferdinand Schober
1c082d5c0c fix win -> lin scancode translations for intl keys 2024-05-17 17:39:52 +02:00
Ferdinand Schober
cd98acbd08 fix right shift not working 2024-05-16 22:36:51 +02:00
Ferdinand Schober
11e1919588 fix most international keybindings 2024-05-16 22:29:41 +02:00
Ferdinand Schober
152bceaa86 fix dns resolving 2024-05-13 08:12:55 +02:00
Ferdinand Schober
60180d841c fix formatting 2024-05-12 15:50:03 +02:00
Ferdinand Schober
5802a0be0b fix discrete scrolling on wlroots 2024-05-12 15:49:39 +02:00
Ferdinand Schober
1737727d61 fix cast 2024-05-12 15:07:29 +02:00
Ferdinand Schober
ba46037a1f fix scrolling in windows 2024-05-12 14:55:56 +02:00
Ferdinand Schober
da768b6fb8 ignore Axis events corresponding to v120 events 2024-05-12 13:32:14 +02:00
Ferdinand Schober
799b45104a enter hook command (#130)
new configuration option `enter_hook` can now be used to spawn a command when a client is entered
2024-05-12 13:01:07 +02:00
Ferdinand Schober
e9738fc024 explicit state synchronisation (#129)
prevents unnecessary state updates and makes synchronization less error prone
2024-05-12 00:49:53 +02:00
Ferdinand Schober
9969f997d3 macos-latest on longer runs on intel macs 2024-05-07 11:46:01 +02:00
Ferdinand Schober
b8cc9e2197 hotfix: race condition when activating clients 2024-05-07 11:18:15 +02:00
Ferdinand Schober
ba6abafe75 windows: fix resolution with scaling enabled (#124) 2024-05-07 00:07:30 +02:00
Ferdinand Schober
effb9ce0fa libei: fix touchpad scrolling (#121) 2024-05-04 12:53:45 +02:00
Ferdinand Schober
1f0d386d4a add discrete120 scroll event (#120)
* add discrete120 scroll event
2024-05-04 03:34:13 +02:00
Ferdinand Schober
973360a774 libei emulation: use discrete scroll events 2024-05-04 01:27:38 +02:00
Ferdinand Schober
e21ab02a6e layer-shell: use value120 scroll events #115 2024-05-04 01:27:13 +02:00
Ferdinand Schober
18a3c10f8e Update FUNDING.yml 2024-05-03 16:40:59 +02:00
Ferdinand Schober
c76d9ef7af implement dns indicator (#119) 2024-05-03 13:00:00 +02:00
Ferdinand Schober
5318f5a02d Separate config state (#118)
* change internal api
* frontend now keeps and more correctly reflects backend state
2024-05-03 11:27:06 +02:00
Johan
1e4312b3ce nix: Add macOS launchd service to hm-module module (#110)
Home manager has the possibility to run macOS launchd services. These
are automatically disabled when running on linux.
2024-04-28 12:25:35 +02:00
Ferdinand Schober
77aa96e09a defer creation of input capture / emulation (#117) 2024-04-26 23:59:00 +02:00
Ferdinand Schober
636c5924bf trust_dns_resolver is now hickory_resolver (#116) 2024-04-26 22:52:00 +02:00
Ferdinand Schober
3e96b42067 use slab instead of reinventing the wheel (#112) 2024-04-26 00:10:04 +02:00
Ferdinand Schober
279e582698 Rename FrontendEvent to FrontendRequest (#111)
* rename frontend event and notify
* simplify event names
2024-04-25 22:18:43 +02:00
Johan
9edd2f7f3b nix: enable creating config file via home-manager (#109) 2024-04-25 14:06:31 +02:00
Johan
43c16a537b nix: Add aarch64-darwin package and devshell (#108)
Enable building and developing using nix on aarch64-darwin
2024-04-24 21:27:24 +02:00
Ferdinand Schober
36855a1a17 pub glib-build-tools behind gtk feature flag 2024-04-15 10:14:48 +02:00
Ferdinand Schober
e537cdbc7e properly reset copy icon 2024-04-13 00:28:53 +02:00
Ferdinand Schober
5b76c3bcda add hostname entry row with clipboard button 2024-04-13 00:24:46 +02:00
Ferdinand Schober
81f65dcd3d gtk: use predifined css classes instead of custom ones 2024-04-13 00:24:46 +02:00
Ferdinand Schober
f0099ee535 windows: revert scrolling multiplier 2024-04-12 15:28:21 +02:00
Ferdinand Schober
633d2c346e windows: impl back and forward mouse buttons 2024-04-12 15:28:05 +02:00
Ferdinand Schober
ccb201ea53 formatting 2024-04-11 13:54:11 +02:00
Ferdinand Schober
f7edfecba9 add tests for capture and emulation 2024-04-11 13:53:49 +02:00
Ferdinand Schober
141ea2809d fix a race condition in windows capture 2024-04-11 13:09:38 +02:00
Ferdinand Schober
058097c618 Update OS support table 2024-04-11 04:05:15 +02:00
Ferdinand Schober
f9eeb254d3 Windows Input Capture (#100)
initial support for windows input capture.
Some things need fixing;
- scrolling
- mouse buttons > 2
2024-04-11 03:55:42 +02:00
Ferdinand Schober
9ca7e2378c enforce only one client at a position (#102) 2024-04-10 14:16:19 +02:00
Ferdinand Schober
cc7984c066 fix clippy lint 2024-04-08 21:14:07 +02:00
Ferdinand Schober
1a2645cfbc fix formatting (#101) 2024-04-08 17:52:16 +02:00
Ferdinand Schober
e52febf457 simplify windows installation instructions 2024-04-08 17:48:22 +02:00
Ferdinand Schober
db96717044 chore: Release lan-mouse version 0.7.3 2024-03-22 12:46:57 +01:00
Ferdinand Schober
be8124a190 fix dns resolving 2024-03-22 12:40:05 +01:00
Ferdinand Schober
dcee2933a2 Create FUNDING.yml 2024-03-22 10:09:00 +01:00
Ferdinand Schober
8aaff9fb58 move to windows from win-api (#99) 2024-03-21 23:04:20 +01:00
Ferdinand Schober
742b1585d7 rename producer, consumer to emulation and capture (#98)
input emulation / input capture is clearer than event consumer and producer
2024-03-21 20:26:57 +01:00
Ferdinand Schober
78c9de45c7 add an arm64 build (#45)
closes #45
2024-03-21 17:14:33 +01:00
Ferdinand Schober
a491c0e9e3 refactor producer and consumer (#97) 2024-03-21 16:55:54 +01:00
Ferdinand Schober
af02cccc2a exit instead of panicing when con to backend lost 2024-03-21 15:35:56 +01:00
Ferdinand Schober
4a6399f866 Update README.md - now available on crates.io 2024-03-21 13:37:01 +01:00
Ferdinand Schober
66bce9083e chore: Release lan-mouse version 0.7.2 2024-03-21 13:06:28 +01:00
Ferdinand Schober
102b64f2b4 chore: Release lan-mouse version 0.7.1 2024-03-21 13:04:22 +01:00
Ferdinand Schober
4b499742ad update dependencies 2024-03-21 13:02:21 +01:00
Ferdinand Schober
a86d74b52c update to reis 0.2 2024-03-21 12:50:47 +01:00
Ferdinand Schober
c25a15e2d8 CI: fix download-artifact job 2024-03-20 15:28:05 +01:00
Ferdinand Schober
8b82325bdb update flake.lock, Cargo.lock 2024-03-20 14:48:29 +01:00
Ferdinand Schober
5415205c83 chore: Release lan-mouse version 0.7.0 2024-03-20 14:31:25 +01:00
Ferdinand Schober
1666fb8b7b Update README.md 2024-03-20 14:18:59 +01:00
Ferdinand Schober
9afe7da0dd Libei Input Capture (#94) 2024-03-20 14:03:52 +01:00
Ferdinand Schober
f7c59e40c9 Update config.toml 2024-03-19 13:14:53 +01:00
Ferdinand Schober
5be5b0ad7c Update README.md 2024-03-19 13:14:36 +01:00
Ferdinand Schober
8ed4520172 Update README.md (#96)
* Update README.md

include release_bind in example config

* update config.toml as well
2024-03-18 09:54:38 +01:00
Ferdinand Schober
9e56c546cd make release bind configurable (#95)
closes #85
2024-03-18 09:20:28 +01:00
Ferdinand Schober
daf8818a9f Update README.md
closes #80
2024-03-17 11:23:32 +01:00
Ferdinand Schober
6eaa199503 update actions (#93) 2024-03-15 22:09:09 +01:00
虢豳
8ff991aefe feat: add nix support(#80) (#82)
* feat: add nix support(#80)

* chore: nix flake update

* update actions
2024-03-15 22:02:18 +01:00
Ferdinand Schober
abf95afb9f Update README.md
Remove Hyprland section - all issues are fixed
2024-03-15 17:27:24 +01:00
Ferdinand Schober
9a75a7622e fix duplicate creating of wl_pointer / wl_keyboard (#92)
closes #91
2024-03-15 17:00:59 +01:00
Ferdinand Schober
0196cfe56c Update README.md
sway 1.9 works
2024-03-03 12:02:38 +01:00
Ferdinand Schober
097468f708 Update README.md
closes #90
2024-03-02 11:58:33 +01:00
Ferdinand Schober
3470abc03a Update README.md
update issues in Hyprland
2024-03-01 13:35:12 +01:00
Ferdinand Schober
a7397ad4f4 Update README.md
fix typo
2024-02-05 16:08:25 +00:00
Ferdinand Schober
9889b49f10 Update README.md 2024-01-30 20:22:51 +01:00
Ferdinand Schober
f4db2366b7 chore: Release lan-mouse version 0.6.0 2024-01-28 17:35:52 +01:00
Ferdinand Schober
c9deb6eba4 add firewalld config file 2024-01-28 17:32:42 +01:00
Kai
5cc8cda19d macos: add keyboard support (#81)
* macos: add keyboard support

* macos: handle key repeat

* update README
2024-01-26 11:05:54 +01:00
Ferdinand Schober
8084b52cfc Revert "gtk: handle exit of service properly"
This reverts commit 1f4821a16d.
breaks ubuntu lts
2024-01-23 21:51:40 +01:00
Ferdinand Schober
1f4821a16d gtk: handle exit of service properly 2024-01-23 21:36:42 +01:00
Ferdinand Schober
82926d8272 fix error handling in consumer task 2024-01-23 20:47:29 +01:00
Ferdinand Schober
006831b9f1 add systemd user service definition
ref #76
ref #49
2024-01-21 20:44:09 +01:00
Ferdinand Schober
e5b770a799 Update README.md
Add installation instructions
2024-01-19 14:14:47 +01:00
Ferdinand Schober
017bc43176 refactor timer task 2024-01-19 02:07:03 +01:00
Ferdinand Schober
36001c6fb2 refactor udp task 2024-01-19 02:03:30 +01:00
Ferdinand Schober
2803db7073 refactor dns task 2024-01-19 02:01:45 +01:00
Ferdinand Schober
622b04b36c refactor frontend task 2024-01-19 01:58:49 +01:00
Ferdinand Schober
61ff05c95a refactor consumer task 2024-01-19 01:51:09 +01:00
Ferdinand Schober
ecab3a360d refactor producer task 2024-01-18 23:46:06 +01:00
Ferdinand Schober
6674af8e63 allow incoming requests from arbitrary ports (#78)
closes #77
2024-01-18 22:36:33 +01:00
Ferdinand Schober
b3caba99ab fix misleading warning 2024-01-18 22:22:27 +01:00
Ferdinand Schober
fad48c2504 no config is not an error 2024-01-17 08:37:18 +01:00
Ferdinand Schober
f28f75418c add a warning when mouse is released by compositor
This can not be influenced and is helpful for debugging
2024-01-17 08:36:41 +01:00
Ferdinand Schober
e2c47d3096 fix: initial dns resolve was not working 2024-01-17 00:22:24 +01:00
Ferdinand Schober
f19944515a Revert "temporary fix for AUR pkg"
This reverts commit 8c276f88b7.
2024-01-16 23:11:13 +01:00
Ferdinand Schober
535cd055b9 fix initial activation 2024-01-16 19:49:34 +01:00
Ferdinand Schober
118c0dfc73 cleanup 2024-01-16 16:58:47 +01:00
Ferdinand Schober
7897db6047 remove unneccessary enumerate request 2024-01-16 16:15:23 +01:00
Ferdinand Schober
347256e966 fix frontend channel buffer size 2024-01-16 16:03:33 +01:00
Ferdinand Schober
0017dbc634 Update README.md 2024-01-16 13:01:25 +01:00
Ferdinand Schober
d90eb0cd0f Activate on startup (#70)
Frontends are now properly synced among each other and on startup the correct state is reflected.

Closes #75 
Closes #68
2024-01-16 12:59:39 +01:00
Ferdinand Schober
2e52660714 fix name of desktop entry 2024-01-15 11:06:00 +01:00
Ferdinand Schober
8c276f88b7 temporary fix for AUR pkg 2024-01-15 09:00:21 +01:00
Ferdinand Schober
13597b3587 fix app_id + app icon 2024-01-15 08:42:23 +01:00
CupricReki
b59808742a Modified .desktop file to conform with standard (#72) 2024-01-15 07:28:38 +01:00
Ferdinand Schober
6c99f9bea3 chore: Release lan-mouse version 0.5.1 2024-01-12 13:46:56 +01:00
Ferdinand Schober
d54b3a08e1 ignore double press / release events (#71)
This should fix most of the remaining issues with stuck keys in KDE
2024-01-12 13:13:23 +01:00
Ferdinand Schober
767fc8bd6b comment about pointer relase in sending state
Figured out, why it sometimes happened that the pointer is released in
sending state. However nothing we can do about it.

(not a real issue)
2024-01-08 16:54:14 +01:00
Ferdinand Schober
fa15048ad8 ignore every event except Enter in receiving mode (#65)
Modifier or other key events are still sent by some compositors after leaving the window which causes them to be pressed on both devices.
Now an Enter event must be produced before any further events are sent out (except disconnect)
2024-01-05 18:00:30 +01:00
Ferdinand Schober
eb366bcd34 Update README.md
fix typos
2024-01-05 17:23:36 +01:00
Ferdinand Schober
91176b1267 Update README.md
see #64
2024-01-05 17:14:03 +01:00
Ferdinand Schober
a6f386ea83 windows: impl key repeating (#63)
closes #47
2024-01-05 13:19:41 +01:00
Ferdinand Schober
40b0cdd52e Add security disclaimer 2024-01-03 16:52:56 +01:00
Ferdinand Schober
1553ed4212 Update README.md 2024-01-02 22:46:27 +01:00
Ferdinand Schober
4561c20610 layer-shell: recreate windows, when output is removed / added (#62)
Previously, when an output was removed (e.g. laptop lid closed and opened) the windows used for input capture would not appear anymore until toggling the client off and on
2024-01-01 22:48:44 +01:00
Ferdinand Schober
6cdb607b11 Fix Error handling in layershell producer (#61)
previous error handling resulted in a softlock when the connection
to the compositor was lost
2024-01-01 22:07:21 +01:00
Ferdinand Schober
f5827bb31c fix port changing 2024-01-01 14:55:29 +01:00
Ferdinand Schober
64e3bf3ff4 release stuck keys (#53)
Keys are now released when
- A client disconnects and still has pressed keys
- A client disconnects through CTLR+ALT+SHIFT+WIN
- Lan Mouse terminates with keys still being pressed through a remote client

This is also fixes an issue caused by KDE's implementation of the remote desktop portal backend:
Keys are not correctly released when a remote desktop session is closed while keys are still pressed,
causing them to be permanently stuck until kwin is restarted.

This workaround remembers all pressed keys and releases them when lan-mouse exits or a device disconnects.

closes #15
2023-12-31 15:42:29 +01:00
Ferdinand Schober
6a6d9a9fa9 chore: Release lan-mouse version 0.5.0 2023-12-28 17:57:04 +01:00
Ferdinand Schober
0fffd5bb15 fix error handling in portchange 2023-12-27 23:44:40 +01:00
Ferdinand Schober
b0df901fcc ci: rename lan-mouse.zip to lan-mouse-windows.zip 2023-12-27 21:58:06 +01:00
Ferdinand Schober
9c0cc98dc0 Remove duplicate code (#60) 2023-12-27 21:40:10 +01:00
Ferdinand Schober
5c152b0cbe ci: fix name-clash with macos binary 2023-12-27 21:39:22 +01:00
Ferdinand Schober
e155819542 unix: send SIGTERM instead of killing the service (#59) 2023-12-27 19:56:43 +01:00
Ferdinand Schober
4b6faea93a add missing exe 2023-12-24 20:47:21 +01:00
Ferdinand Schober
80d8a496bb zip windows files 2023-12-24 20:09:28 +01:00
Ferdinand Schober
53e1af0780 include dlls in release 2023-12-24 18:54:40 +01:00
Ferdinand Schober
d3fed1b769 enable gtk frontend in windows (#58)
The gtk frontend can now be built in windows!
The github workflow is updated to build GTK and add it to the releases section.
2023-12-24 18:00:59 +01:00
Ferdinand Schober
cdd3a3b818 Split tasks - event loop now properly asynchronous (#57)
DNS, etc. does no longer block the event loop
2023-12-23 14:46:38 +01:00
Ferdinand Schober
1cefa38543 hotfix: Dont stall the event loop if udp blocks 2023-12-22 17:45:20 +01:00
Ferdinand Schober
fed8e02d9f Update dependencies (#56)
update wayland-client + reis
2023-12-22 14:50:12 +01:00
Ferdinand Schober
65a12735e2 add missing release() on event producer 2023-12-22 13:32:39 +01:00
Ferdinand Schober
3484cab28c Update Roadmap 2023-12-20 11:35:49 +01:00
Ferdinand Schober
256d2107bd downgrade libadwaita
AdwToolbarView is available starting with libadwaita 1.4,
which is very bleeding-edge
2023-12-19 22:09:12 +01:00
Ferdinand Schober
3ac738fb52 fix formatting 2023-12-19 21:29:38 +01:00
Ferdinand Schober
2ac9277fb0 macos: impl relative mouse motion
this fixes mouse movement in games
2023-12-19 21:25:29 +01:00
Ferdinand Schober
d9fa86ef00 cli: wait for connection (#55) 2023-12-19 17:22:39 +01:00
Ferdinand Schober
015facec39 formatting 2023-12-18 21:41:43 +01:00
Ferdinand Schober
66de3e3cbc fix remaining clippy lints 2023-12-18 21:38:12 +01:00
Ferdinand Schober
4600db7af8 add cargo fmt + cargo clippy to rust workflow 2023-12-18 21:27:53 +01:00
Ferdinand Schober
9f23e1a75d remove unused code 2023-12-18 20:54:06 +01:00
Ferdinand Schober
a24b231e3c Update README.md 2023-12-18 18:14:28 +01:00
Ferdinand Schober
8de6c9bb87 Implement keycode translation for windows (#54)
closes #48 
closes #16
2023-12-18 18:08:10 +01:00
Ferdinand Schober
6766886377 Fix Keycodes in X11
Keycodes are now correctly offset for X11
2023-12-18 09:52:42 +01:00
Ferdinand Schober
a6ab109fae remove kde-fake-input backend 2023-12-17 19:33:40 +01:00
Ferdinand Schober
06c4e92d43 dont panic on unavailable compositor 2023-12-17 19:33:40 +01:00
Ferdinand Schober
f5a0ff4f3a Add a dummy backend 2023-12-17 19:33:40 +01:00
Ferdinand Schober
eca367cdb4 Fix an issue where client could not be entered again (#51)
RELEASE_MODIFIERS are now handled server side.

A client exited through CTRL+ALT+SHIFT+SUPER could sometimes not be entered again when the client
did not send a Modifiers (0) event.

Such a client would always respond with Modifiers (RELEASE_MODIFIERS) and immediately cause the sender to
exit again.

To prevent this, a modifier event with all modifiers released is now sent instead when the release modifiers are detected.
2023-12-17 17:57:17 +01:00
Ferdinand Schober
735434438f add leave event to make entering a client more reliable (#50)
Instead of relying on release events not getting lost, every event now signals the opponent
to release its pointer grab.

There is one case that requires a Leave event:

Consider a Sending client A and receiving Client B.

If B enters the dead-zone of client A, it will send an enter event towards A but before
A receives the Release event, it may still send additional events towards B that should not cause
B to immediately revert to Receiving state again.

Therefore B puts itself into AwaitingLeave state until it receives a Leave event coming from A.
A responds to the Enter event coming from B with a leave event, to signify that it will no longer
send any events and releases it's pointer.

To guard against packet loss of the leave events, B sends additional enter events while it is in AwaitingLeave
mode until it receives a Leave event at some point.

This is still not resilient against possible packet reordering in UDP but in the (rare) case where a leave event arrives before some other event coming from A, the user would simply need to move the pointer into the dead-zone again.
2023-12-17 17:38:06 +01:00
Ferdinand Schober
02d1b33e45 remove unused warnings 2023-12-17 16:08:18 +01:00
Ferdinand Schober
19143b90a5 add debug information to xdgrdp backend 2023-12-17 12:23:03 +01:00
Ferdinand Schober
ad2aeae275 make libc optional 2023-12-16 13:08:24 +01:00
Ferdinand Schober
0bdb1bc753 Update README 2023-12-16 12:14:07 +01:00
Ferdinand Schober
9140f60c69 X11: impl keyboard events (still disabled)
Needs keycode translation
2023-12-16 12:11:43 +01:00
Ferdinand Schober
0fbf9f4dc2 X11: Mouse emulation now fully supported 2023-12-16 11:48:06 +01:00
Ferdinand Schober
f13e25af82 fix: Dont release mouse on all events
When received motion events lead to entering the dead zone,
we set the state to sending but there may still be motion
events on the way so we must ignore those.
2023-12-16 10:13:14 +01:00
Ferdinand Schober
48f7ad3592 simplify enumerate 2023-12-15 20:47:51 +01:00
Ferdinand Schober
5fc02d471b cleanup server code 2023-12-15 20:24:14 +01:00
Ferdinand Schober
0c275bc2de remove duplicate log messages when ignoring events 2023-12-15 08:29:30 +01:00
Ferdinand Schober
010db79918 Update version number 2023-12-11 11:39:35 +01:00
Ferdinand Schober
ebf5a64f20 address clippy lints 2023-12-11 11:39:20 +01:00
CupricReki
5c8ea25563 Added example .desktop file. (#40)
* Added example .desktop file.

* remove Path (not required anymore)

* remove "Test" from name

* add German name

* Update description to match GitHub description

---------

Co-authored-by: Ferdinand Schober <ferdinandschober20@gmail.com>
2023-12-10 23:15:53 +01:00
Ferdinand Schober
b472b56b10 fix a compiler warning 2023-12-10 14:26:05 +01:00
Ferdinand Schober
18edb0dbad update screenshot light / dark 2023-12-10 13:17:32 +01:00
Ferdinand Schober
622b820c7f chore: Release lan-mouse version 0.4.0 2023-12-09 02:13:26 +01:00
Ferdinand Schober
09bf535eec Update README.md 2023-12-09 02:06:42 +01:00
Ferdinand Schober
39acce8e6a Update README.md 2023-12-09 02:03:18 +01:00
Ferdinand Schober
e3f9947284 macos: enable running lan-mouse on macos (#42)
* macos: initial support

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

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

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

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

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

* fix scrolling (TODO: smooth / kinetic scrolling)

* windows: fix compilation errors

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

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

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

1
.envrc Normal file
View File

@@ -0,0 +1 @@
use flake

2
.github/FUNDING.yml vendored Normal file
View File

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

40
.github/workflows/cachix.yml vendored Normal file
View File

@@ -0,0 +1,40 @@
name: Binary Cache
on: [push, pull_request, workflow_dispatch]
jobs:
nix:
strategy:
matrix:
os:
- ubuntu-latest
- macos-13
- macos-14
name: "Build"
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: recursive
- uses: DeterminateSystems/nix-installer-action@main
with:
logger: pretty
- uses: DeterminateSystems/magic-nix-cache-action@main
- uses: cachix/cachix-action@v14
with:
name: lan-mouse
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
- name: Build lan-mouse (x86_64-linux)
if: matrix.os == 'ubuntu-latest'
run: nix build --print-build-logs --show-trace .#packages.x86_64-linux.lan-mouse
- name: Build lan-mouse (x86_64-darwin)
if: matrix.os == 'macos-13'
run: nix build --print-build-logs --show-trace .#packages.x86_64-darwin.lan-mouse
- name: Build lan-mouse (aarch64-darwin)
if: matrix.os == 'macos-14'
run: nix build --print-build-logs --show-trace .#packages.aarch64-darwin.lan-mouse

View File

@@ -13,15 +13,16 @@ jobs:
linux-release-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: install dependencies
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
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: lan-mouse-linux
path: target/release/lan-mouse
@@ -29,22 +30,94 @@ jobs:
windows-release-build:
runs-on: windows-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v5
with:
python-version: '3.11'
# needed for cache restore
- name: create gtk dir
run: mkdir C:\gtk-build\gtk\x64\release
- uses: actions/cache@v3
id: cache
with:
path: c:/gtk-build/gtk/x64/release/**
key: gtk-windows-build
restore-keys: gtk-windows-build
- name: Update path
run: |
echo "PKG_CONFIG=C:\gtk-build\gtk\x64\release\bin\pkgconf.exe" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
echo "C:\pkg-config-lite-0.28-1\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
echo "C:\gtk-build\gtk\x64\release\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
echo $env:GITHUB_PATH
echo $env:PATH
- name: Install dependencies
if: steps.cache.outputs.cache-hit != 'true'
run: |
# choco install msys2
# choco install visualstudio2022-workload-vctools
# choco install pkgconfiglite
py -m venv .venv
.venv\Scripts\activate.ps1
py -m pip install gvsbuild
# see https://github.com/wingtk/gvsbuild/pull/1004
Move-Item "C:\Program Files\Git\usr\bin" "C:\Program Files\Git\usr\notbin"
Move-Item "C:\Program Files\Git\bin" "C:\Program Files\Git\notbin"
gvsbuild build --msys-dir=C:\msys64 gtk4 libadwaita librsvg
Move-Item "C:\Program Files\Git\usr\notbin" "C:\Program Files\Git\usr\bin"
Move-Item "C:\Program Files\Git\notbin" "C:\Program Files\Git\bin"
- uses: actions/checkout@v4
- name: Release Build
run: cargo build --release
- name: Create Archive
run: |
mkdir "lan-mouse-windows"
Get-Childitem -Path "C:\\gtk-build\\gtk\\x64\\release\\bin\\*.dll" -File -Recurse | Copy-Item -Destination "lan-mouse-windows"
Copy-Item -Path "target\release\lan-mouse.exe" -Destination "lan-mouse-windows"
Compress-Archive -Path "lan-mouse-windows\*" -DestinationPath lan-mouse-windows.zip
- name: Upload build artifact
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: lan-mouse-windows
path: target/release/lan-mouse.exe
path: lan-mouse-windows.zip
macos-release-build:
runs-on: macos-13
steps:
- uses: actions/checkout@v4
- name: install dependencies
run: brew install gtk4 libadwaita
- name: Release Build
run: |
cargo build --release
cp target/release/lan-mouse lan-mouse-macos-intel
- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
name: lan-mouse-macos-intel
path: lan-mouse-macos-intel
macos-aarch64-release-build:
runs-on: macos-14
steps:
- uses: actions/checkout@v4
- name: install dependencies
run: brew install gtk4 libadwaita
- name: Release Build
run: |
cargo build --release
cp target/release/lan-mouse lan-mouse-macos-aarch64
- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
name: lan-mouse-macos-aarch64
path: lan-mouse-macos-aarch64
pre-release:
name: "Pre Release"
needs: [windows-release-build, linux-release-build]
needs: [windows-release-build, linux-release-build, macos-release-build]
runs-on: "ubuntu-latest"
steps:
- name: Download build artifacts
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
- name: Create Release
uses: "marvinpinto/action-automatic-releases@latest"
with:
@@ -54,4 +127,6 @@ jobs:
title: "Development Build"
files: |
lan-mouse-linux/lan-mouse
lan-mouse-windows/lan-mouse.exe
lan-mouse-macos-intel/lan-mouse-macos-intel
lan-mouse-macos-aarch64/lan-mouse-macos-aarch64
lan-mouse-windows/lan-mouse-windows.zip

View File

@@ -15,17 +15,22 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: install dependencies
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
run: cargo test --verbose
- name: Check Formatting
run: cargo fmt --check
- name: Clippy
run: cargo clippy --all-features --all-targets -- --deny warnings
- name: Upload build artifact
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: lan-mouse
path: target/debug/lan-mouse
@@ -35,13 +40,95 @@ jobs:
runs-on: windows-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
# needed for cache restore
- name: create gtk dir
run: mkdir C:\gtk-build\gtk\x64\release
- uses: actions/cache@v3
id: cache
with:
path: c:/gtk-build/gtk/x64/release/**
key: gtk-windows-build
restore-keys: gtk-windows-build
- name: Update path
run: |
echo "PKG_CONFIG=C:\gtk-build\gtk\x64\release\bin\pkgconf.exe" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
echo "C:\pkg-config-lite-0.28-1\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
echo "C:\gtk-build\gtk\x64\release\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
echo $env:GITHUB_PATH
echo $env:PATH
- name: Install dependencies
if: steps.cache.outputs.cache-hit != 'true'
run: |
# choco install msys2
# choco install visualstudio2022-workload-vctools
# choco install pkgconfiglite
py -m venv .venv
.venv\Scripts\activate.ps1
py -m pip install gvsbuild
# see https://github.com/wingtk/gvsbuild/pull/1004
Move-Item "C:\Program Files\Git\usr\bin" "C:\Program Files\Git\usr\notbin"
Move-Item "C:\Program Files\Git\bin" "C:\Program Files\Git\notbin"
gvsbuild build --msys-dir=C:\msys64 gtk4 libadwaita librsvg
Move-Item "C:\Program Files\Git\usr\notbin" "C:\Program Files\Git\usr\bin"
Move-Item "C:\Program Files\Git\notbin" "C:\Program Files\Git\bin"
- name: Build
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose
- name: Check Formatting
run: cargo fmt --check
- name: Clippy
run: cargo clippy --all-features --all-targets -- --deny warnings
- name: Copy Gtk Dlls
run: Get-Childitem -Path "C:\\gtk-build\\gtk\\x64\\release\\bin\\*.dll" -File -Recurse | Copy-Item -Destination "target\\debug"
- name: Upload build artifact
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: lan-mouse-windows
path: target/debug/lan-mouse.exe
path: |
target/debug/lan-mouse.exe
target/debug/*.dll
build-macos:
runs-on: macos-13
steps:
- uses: actions/checkout@v4
- name: install dependencies
run: brew install gtk4 libadwaita
- name: Build
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose
- name: Check Formatting
run: cargo fmt --check
- name: Clippy
run: cargo clippy --all-features --all-targets -- --deny warnings
- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
name: lan-mouse-macos
path: target/debug/lan-mouse
build-macos-aarch64:
runs-on: macos-14
steps:
- uses: actions/checkout@v4
- name: install dependencies
run: brew install gtk4 libadwaita
- name: Build
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose
- name: Check Formatting
run: cargo fmt --check
- name: Clippy
run: cargo clippy --all-features --all-targets -- --deny warnings
- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
name: lan-mouse-macos-aarch64
path: target/debug/lan-mouse

View File

@@ -9,15 +9,16 @@ jobs:
linux-release-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: install dependencies
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
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: lan-mouse-linux
path: target/release/lan-mouse
@@ -25,22 +26,94 @@ jobs:
windows-release-build:
runs-on: windows-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v5
with:
python-version: '3.11'
# needed for cache restore
- name: create gtk dir
run: mkdir C:\gtk-build\gtk\x64\release
- uses: actions/cache@v3
id: cache
with:
path: c:/gtk-build/gtk/x64/release/**
key: gtk-windows-build
restore-keys: gtk-windows-build
- name: Update path
run: |
echo "PKG_CONFIG=C:\gtk-build\gtk\x64\release\bin\pkgconf.exe" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
echo "C:\pkg-config-lite-0.28-1\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
echo "C:\gtk-build\gtk\x64\release\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
echo $env:GITHUB_PATH
echo $env:PATH
- name: Install dependencies
if: steps.cache.outputs.cache-hit != 'true'
run: |
# choco install msys2
# choco install visualstudio2022-workload-vctools
# choco install pkgconfiglite
py -m venv .venv
.venv\Scripts\activate.ps1
py -m pip install gvsbuild
# see https://github.com/wingtk/gvsbuild/pull/1004
Move-Item "C:\Program Files\Git\usr\bin" "C:\Program Files\Git\usr\notbin"
Move-Item "C:\Program Files\Git\bin" "C:\Program Files\Git\notbin"
gvsbuild build --msys-dir=C:\msys64 gtk4 libadwaita librsvg
Move-Item "C:\Program Files\Git\usr\notbin" "C:\Program Files\Git\usr\bin"
Move-Item "C:\Program Files\Git\notbin" "C:\Program Files\Git\bin"
- uses: actions/checkout@v4
- name: Release Build
run: cargo build --release
- name: Create Archive
run: |
mkdir "lan-mouse-windows"
Get-Childitem -Path "C:\\gtk-build\\gtk\\x64\\release\\bin\\*.dll" -File -Recurse | Copy-Item -Destination "lan-mouse-windows"
Copy-Item -Path "target\release\lan-mouse.exe" -Destination "lan-mouse-windows"
Compress-Archive -Path "lan-mouse-windows\*" -DestinationPath lan-mouse-windows.zip
- name: Upload build artifact
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: lan-mouse-windows
path: target/release/lan-mouse.exe
path: lan-mouse-windows.zip
macos-release-build:
runs-on: macos-13
steps:
- uses: actions/checkout@v4
- name: install dependencies
run: brew install gtk4 libadwaita
- name: Release Build
run: |
cargo build --release
cp target/release/lan-mouse lan-mouse-macos-intel
- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
name: lan-mouse-macos-intel
path: lan-mouse-macos-intel
macos-aarch64-release-build:
runs-on: macos-14
steps:
- uses: actions/checkout@v4
- name: install dependencies
run: brew install gtk4 libadwaita
- name: Release Build
run: |
cargo build --release
cp target/release/lan-mouse lan-mouse-macos-aarch64
- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
name: lan-mouse-macos-aarch64
path: lan-mouse-macos-aarch64
tagged-release:
name: "Tagged Release"
needs: [windows-release-build, linux-release-build]
needs: [windows-release-build, linux-release-build, macos-release-build]
runs-on: "ubuntu-latest"
steps:
- name: Download build artifacts
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
- name: "Create Release"
uses: "marvinpinto/action-automatic-releases@latest"
with:
@@ -48,4 +121,6 @@ jobs:
prerelease: false
files: |
lan-mouse-linux/lan-mouse
lan-mouse-windows/lan-mouse.exe
lan-mouse-macos-intel/lan-mouse-macos-intel
lan-mouse-macos-aarch64/lan-mouse-macos-aarch64
lan-mouse-windows/lan-mouse-windows.zip

6
.gitignore vendored
View File

@@ -1 +1,7 @@
/target
.gdbinit
.idea/
.vs/
.vscode/
.direnv/
result

2296
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,48 +1,77 @@
[workspace]
members = [
"input-capture",
"input-emulation",
"input-event",
"lan-mouse-ipc",
"lan-mouse-cli",
"lan-mouse-gtk",
"lan-mouse-proto",
]
[package]
name = "lan-mouse"
description = "Software KVM Switch / mouse & keyboard sharing software for Local Area Networks"
version = "0.2.0"
version = "0.10.0"
edition = "2021"
license = "GPL-3.0-or-later"
repository = "https://github.com/ferdinandschober/lan-mouse"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
repository = "https://github.com/feschber/lan-mouse"
[profile.release]
strip = true
lto = "fat"
[dependencies]
tempfile = "3.6"
trust-dns-resolver = "0.22"
memmap = "0.7"
toml = "0.7"
input-event = { path = "input-event", version = "0.3.0" }
input-emulation = { path = "input-emulation", version = "0.3.0", default-features = false }
input-capture = { path = "input-capture", version = "0.3.0", default-features = false }
lan-mouse-cli = { path = "lan-mouse-cli", version = "0.2.0" }
lan-mouse-gtk = { path = "lan-mouse-gtk", version = "0.2.0", optional = true }
lan-mouse-ipc = { path = "lan-mouse-ipc", version = "0.2.0" }
lan-mouse-proto = { path = "lan-mouse-proto", version = "0.2.0" }
hickory-resolver = "0.24.1"
toml = "0.8"
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"
env_logger = "0.11.3"
serde_json = "1.0.107"
tokio = { version = "1.32.0", features = [
"io-util",
"io-std",
"macros",
"net",
"process",
"rt",
"sync",
"signal",
] }
futures = "0.3.28"
clap = { version = "4.4.11", features = ["derive"] }
slab = "0.4.9"
thiserror = "2.0.0"
tokio-util = "0.7.11"
local-channel = "0.1.5"
[target.'cfg(unix)'.dependencies]
wayland-client = { version="0.30.2", optional = true }
wayland-protocols = { version="0.30.0", features=["client", "staging", "unstable"], optional = true }
wayland-protocols-wlr = { version="0.1.0", features=["client"], optional = true }
wayland-protocols-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_8"], optional = true }
adw = { package = "libadwaita", version = "0.5.2", features = ["v1_3"], optional = true }
[target.'cfg(windows)'.dependencies]
winapi = { version = "0.3.9", features = ["winuser"] }
libc = "0.2.148"
[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"]
xdg_desktop_portal = []
libei = []
gtk = ["dep:gtk", "dep:adw"]
default = [
"gtk",
"layer_shell_capture",
"x11_capture",
"libei_capture",
"wlroots_emulation",
"libei_emulation",
"rdp_emulation",
"x11_emulation",
]
gtk = ["dep:lan-mouse-gtk"]
layer_shell_capture = ["input-capture/layer_shell"]
x11_capture = ["input-capture/x11"]
libei_capture = ["input-event/libei", "input-capture/libei"]
libei_emulation = ["input-event/libei", "input-emulation/libei"]
wlroots_emulation = ["input-emulation/wlroots"]
x11_emulation = ["input-emulation/x11"]
rdp_emulation = ["input-emulation/remote_desktop_portal"]

550
README.md
View File

@@ -1,209 +1,413 @@
# Lan Mouse Share
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/).
# Lan Mouse
Lan Mouse is a *cross-platform* mouse and keyboard sharing software similar to universal-control on Apple devices.
It allows for using multiple PCs via a single set of mouse and keyboard.
This is also known as a Software KVM switch.
Focus lies on performance and a clean, manageable implementation that can easily be expanded to support additional backends like e.g. Android, iOS, ... .
Goal of this project is to be an open-source alternative to proprietary tools like [Synergy 2/3](https://symless.com/synergy), [Share Mouse](https://www.sharemouse.com/de/)
and an alternative to other open source tools like [Deskflow](https://github.com/deskflow/deskflow) or [Input Leap](https://github.com/input-leap) (Synergy fork).
Of course ***blazingly fast™*** and stable, because it's written in rust.
Focus lies on performance, ease of use and a maintainable implementation that can be expanded to support additional backends for e.g. Android, iOS, ... in the future.
For an alternative (with slightly different goals) you may check out [Input Leap](https://github.com/input-leap).
***blazingly fast™*** because it's written in rust.
## Configuration
Configuration is done through the file `config.toml`,
which must be located in the current working directory when
executing lan-mouse.
- _Now with a gtk frontend_
### Example config
A minimal config file could look like this:
<picture>
<source media="(prefers-color-scheme: dark)" srcset="/screenshots/dark.png?raw=true">
<source media="(prefers-color-scheme: light)" srcset="/screenshots/light.png?raw=true">
<img alt="Screenshot of Lan-Mouse" srcset="/screenshots/dark.png">
</picture>
```toml
[left]
host_name = "my-laptop"
> [!WARNING]
> DISCLAIMER:
> Until [#200](https://github.com/feschber/lan-mouse/pull/200) is merged, all network traffic is **unencrypted** and sent in **plaintext**!
>
> A malicious actor with access to the network could read input data or send input events with spoofed IPs to take control over a device.
>
> Therefore you should only use this tool in your local network with trusted devices.
> I take no responsibility for any security breaches!
## OS Support
Most current desktop environments and operating systems are fully supported, this includes
- GNOME >= 45
- KDE Plasma >= 6.1
- Most wlroots based compositors, including Sway (>= 1.8), Hyprland and Wayfire
- Windows
- MacOS
### Caveats / Known Issues
> [!Important]
> - **X11** currently only has support for input emulation, i.e. can only be used on the receiving end.
>
> - **Sway / wlroots**: Wlroots based compositors without libei support on the receiving end currently do not handle modifier events on the client side.
> This results in CTRL / SHIFT / ALT / SUPER keys not working with a sending device that is NOT using the `layer-shell` backend
>
> - **Wayfire**: If you are using [Wayfire](https://github.com/WayfireWM/wayfire), make sure to use a recent version (must be newer than October 23rd) and **add `shortcuts-inhibit` to the list of plugins in your wayfire config!**
> Otherwise input capture will not work.
>
> - **Windows**: The mouse cursor will be invisible when sending input to a Windows system if
> there is no real mouse connected to the machine.
For more detailed information about os support see [Detailed OS Support](#detailed-os-support)
## Installation
<details>
<summary>Arch Linux</summary>
Lan Mouse can be installed from the [official repositories](https://archlinux.org/packages/extra/x86_64/lan-mouse/):
```sh
pacman -S lan-mouse
```
Where `left` can be either `left`, `right`, `top` or `bottom`.
The prerelease version (following `main`) is available on the AUR:
### 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
```sh
paru -S lan-mouse-git
```
</details>
<details>
<summary>Nix (OS)</summary>
- nixpkgs: [search.nixos.org](https://search.nixos.org/packages?channel=unstable&show=lan-mouse&from=0&size=50&sort=relevance&type=packages&query=lan-mouse)
- flake: [README.md](./nix/README.md)
</details>
<details>
<summary>Manual Installation</summary>
First make sure to [install the necessary dependencies](#installing-dependencies).
Precompiled release binaries for Windows, MacOS and Linux are available in the [releases section](https://github.com/feschber/lan-mouse/releases).
For Windows, the depenedencies are included in the .zip file, for other operating systems see [Installing Dependencies](#installing-dependencies).
Alternatively, the `lan-mouse` binary can be compiled from source (see below).
### Installing desktop file, app icon and firewall rules (optional)
```sh
# install lan-mouse (replace path/to/ with the correct path)
sudo cp path/to/lan-mouse /usr/local/bin/
# install app icon
sudo mkdir -p /usr/local/share/icons/hicolor/scalable/apps
sudo cp lan-mouse-gtk/resources/de.feschber.LanMouse.svg /usr/local/share/icons/hicolor/scalable/apps
# update icon cache
gtk-update-icon-cache /usr/local/share/icons/hicolor/
# install desktop entry
sudo mkdir -p /usr/local/share/applications
sudo cp de.feschber.LanMouse.desktop /usr/local/share/applications
# when using firewalld: install firewall rule
sudo cp firewall/lan-mouse.xml /etc/firewalld/services
# -> enable the service in firewalld settings
```
Instead of downloading from the releases, the `lan-mouse` binary
can be easily compiled via cargo or nix:
### Compiling and installing manually:
```sh
# compile in release mode
cargo build --release
# install lan-mouse
sudo cp target/release/lan-mouse /usr/local/bin/
```
Run
### Compiling and installing via cargo:
```sh
cargo run --release
# will end up in ~/.cargo/bin
cargo install lan-mouse
```
### Conditional Compilation
### Compiling and installing via nix:
```sh
# you can find the executable in result/bin/lan-mouse
nix-build
```
### Conditional compilation
Support for other platforms is omitted automatically based on the active
rust toolchain.
Currently only x11, wayland and windows are supported backends,
Depending on the toolchain used, support for other platforms is omitted
automatically (it does not make sense to build a Windows `.exe` with
support for x11 and wayland backends).
However one might still want to omit support for e.g. wayland or x11 on
a Linux system.
This is possible through
[cargo features](https://doc.rust-lang.org/cargo/reference/features.html)
Additionally, available backends and frontends can be configured manually via
[cargo features](https://doc.rust-lang.org/cargo/reference/features.html).
E.g. if only wayland support is needed, the following command produces
an executable with just support for wayland:
```sh
cargo build --no-default-features --features wayland
```
## OS Support
The following table shows support for Event receiving and event Emitting
on different operating systems:
| Backend | Event Receiving | Event Emitting |
|---------------------------|--------------------------|--------------------------------------|
| Wayland (wlroots) | :heavy_check_mark: | :heavy_check_mark: |
| Wayland (KDE) | WIP | :heavy_check_mark: |
| Wayland (Gnome) | TODO (libei support) | TODO (wlr-layer-shell not supported) |
| X11 | WIP | TODO |
| Windows | needs improvements | TODO |
| MacOS | TODO (I dont own a Mac) | TODO (I dont own a Mac) |
## Wayland compositor support
### Input Emulation (for receiving events)
On wayland input-emulation is in an early/unstable state as of writing this.
Different compositors have different ways of enabling input emulation:
Most wlroots-based compositors like Hyprland and Sway support the following
unstable wayland protocols for keyboard and mouse emulation:
- [virtual-keyboard-unstable-v1](https://wayland.app/protocols/virtual-keyboard-unstable-v1)
- [wlr-virtual-pointer-unstable-v1](https://wayland.app/protocols/wlr-virtual-pointer-unstable-v1) are used to emulate input on wlroots compositors
KDE also has a protocol for input emulation ([kde-fake-input](https://wayland.app/protocols/kde-fake-input)), it is however not exposed to
third party apps, so the recommended way of enabling input emulation in KDE is the
[freedesktop remote-desktop-portal](https://flatpak.github.io/xdg-desktop-portal/#gdbus-org.freedesktop.portal.RemoteDesktop).
Gnome uses [libei](https://gitlab.freedesktop.org/libinput/libei) for input emulation,
which has the goal to become the general approach for emulating Input on wayland.
| Required Protocols (Event Receiving) | Sway | Kwin | Gnome |
|----------------------------------------|--------------------|----------------------|----------------------|
| wlr-virtual-pointer-unstable-v1 | :heavy_check_mark: | :x: | :x: |
| virtual-keyboard-unstable-v1 | :heavy_check_mark: | :x: | :x: |
| ~fake-input~ | :x: | ~:heavy_check_mark:~ | :x: |
### Input capture
To capture mouse and keyboard input, a few things are necessary:
- Displaying an immovable surface at screen edges
- Locking the mouse in place
- (optionally but highly recommended) reading unaccelerated mouse input
| Required Protocols (Event Emitting) | Sway | Kwin | Gnome |
|----------------------------------------|--------------------|----------------------|----------------------|
| pointer-constraints-unstable-v1 | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| relative-pointer-unstable-v1 | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| keyboard-shortcuts-inhibit-unstable-v1 | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| wlr-layer-shell-unstable-v1 | :heavy_check_mark: | :heavy_check_mark: | :x: |
The [zwlr\_virtual\_pointer\_manager\_v1](wlr-virtual-pointer-unstable-v1) is required
to display surfaces on screen edges and used to display the immovable window on
both wlroots based compositors and KDE.
Gnome unfortunately does not support this protocol
and [likely won't ever support it](https://gitlab.gnome.org/GNOME/gnome-shell/-/issues/1141).
So there is currently no way of doing this in Wayland, aside from a custom Gnome-Shell
extension, which is not a very elegant solution.
This is to be looked into in the future.
~In order for layershell surfaces to be able to lock the pointer using the pointer\_constraints protocol [this patch](https://github.com/swaywm/sway/pull/7178) needs to be applied to sway.~
(this works natively on sway versions >= 1.8)
## Windows support
Currently windows can receive mouse and keyboard events, however unlike
with the wlroots back-end,
the scancodes are not translated between keyboard layouts.
Event emitting is WIP.
For a detailed list of available features, checkout the [Cargo.toml](./Cargo.toml)
</details>
## TODOS
- [x] Capture the actual mouse events on the server side via a wayland client and send them to the client
- [x] Mouse grabbing
- [x] Window with absolute position -> wlr\_layer\_shell
- [x] DNS resolving
- [x] Keyboard support
- [x] Scrollwheel support
- [x] Button support
- [ ] Latency measurement + logging
- [ ] Bandwidth usage approximation + logging
- [ ] 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)
## Installing Dependencies for Development / Compiling from Source
<details>
<summary>MacOS</summary>
```sh
brew install libadwaita pkg-config
```
</details>
<details>
<summary>Ubuntu and derivatives</summary>
```sh
sudo apt install libadwaita-1-dev libgtk-4-dev libx11-dev libxtst-dev
```
</details>
<details>
<summary>Arch and derivatives</summary>
```sh
sudo pacman -S libadwaita gtk libx11 libxtst
```
</details>
<details>
<summary>Fedora and derivatives</summary>
```sh
sudo dnf install libadwaita-devel libXtst-devel libX11-devel
```
</details>
<details>
<summary>Nix</summary>
```sh
nix-shell .
```
</details>
<details>
<summary>Nix (flake)</summary>
```sh
nix develop
```
</details>
<details>
<summary>Windows</summary>
- First install [Rust](https://www.rust-lang.org/tools/install).
- Then follow the instructions at [gtk-rs.org](https://gtk-rs.org/gtk4-rs/stable/latest/book/installation_windows.html)
*TLDR:*
Build gtk from source
- The following commands should be run in an **admin power shell** instance:
```sh
# install chocolatey
Set-ExecutionPolicy Bypass -Scope Process -Force; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))
# install gvsbuild dependencies
choco install python git msys2 visualstudio2022-workload-vctools
```
- The following commands should be run in a **regular power shell** instance:
```sh
# install gvsbuild with python
python -m pip install --user pipx
python -m pipx ensurepath
```
- Relaunch your powershell instance so the changes in the environment are reflected.
```sh
pipx install gvsbuild
# build gtk + libadwaita
gvsbuild build gtk4 libadwaita librsvg adwaita-icon-theme
```
- **Make sure to add the directory** `C:\gtk-build\gtk\x64\release\bin`
[**to the `PATH` environment variable**]((https://learn.microsoft.com/en-us/previous-versions/office/developer/sharepoint-2010/ee537574(v=office.14))). Otherwise the project will fail to build.
To avoid building GTK from source, it is possible to disable
the gtk frontend (see conditional compilation).
</details>
## Usage
<details>
<summary>Gtk Frontend</summary>
By default the gtk frontend will open when running `lan-mouse`.
To add a new connection, simply click the `Add` button on *both* devices,
enter the corresponding hostname and activate it.
If the mouse can not be moved onto a device, make sure you have port `4242` (or the one selected)
opened up in your firewall.
</details>
<details>
<summary>Command Line Interface</summary>
The cli interface can be enabled using `--frontend cli` as commandline arguments.
Type `help` to list the available commands.
E.g.:
```sh
$ cargo run --release -- --frontend cli
(...)
> connect <host> left|right|top|bottom
(...)
> list
(...)
> activate 0
```
</details>
<details>
<summary>Daemon Mode</summary>
Lan Mouse can be launched in daemon mode to keep it running in the background (e.g. for use in a systemd-service).
To do so, add `--daemon` to the commandline args:
```sh
lan-mouse --daemon
```
In order to start lan-mouse with a graphical session automatically,
the [systemd-service](service/lan-mouse.service) can be used:
Copy the file to `~/.config/systemd/user/` and enable the service:
```sh
cp service/lan-mouse.service ~/.config/systemd/user
systemctl --user daemon-reload
systemctl --user enable --now lan-mouse.service
```
</details>
## Configuration
To automatically load clients on startup, the file `$XDG_CONFIG_HOME/lan-mouse/config.toml` is parsed.
`$XDG_CONFIG_HOME` defaults to `~/.config/`.
To create this file you can copy the following example config:
### Example config
> [!TIP]
> key symbols in the release bind are named according
> to their names in [src/scancode.rs#L172](src/scancode.rs#L172).
> This is bound to change
```toml
# example configuration
# configure release bind
release_bind = [ "KeyA", "KeyS", "KeyD", "KeyF" ]
# 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
hostname = "iridium"
# activate this client immediately when lan-mouse is started
activate_on_startup = true
# 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]
# The hostname is optional: When no hostname is specified,
# at least one ip address needs to be specified.
hostname = "thorium"
# ips for ethernet and wifi
ips = ["192.168.178.189", "192.168.178.172"]
# optional port
port = 4242
```
Where `left` can be either `left`, `right`, `top` or `bottom`.
## Roadmap
- [x] Graphical frontend (gtk + libadwaita)
- [x] respect xdg-config-home for config file location.
- [x] IP Address switching
- [x] Liveness tracking Automatically ungrab mouse when client unreachable
- [x] Liveness tracking: Automatically release keys, when server offline
- [x] MacOS KeyCode Translation
- [x] Libei Input Capture
- [x] MacOS Input Capture
- [x] Windows Input Capture
- [ ] *Encryption* (WIP)
- [ ] X11 Input Capture
- [ ] Latency measurement and visualization
- [ ] Bandwidth usage measurement and visualization
- [ ] Clipboard support
- [ ] Graphical frontend (gtk?)
- [ ] *Encrytion*
- [ ] Gnome Shell Extension (layer shell is not supported)
- [ ] respect xdg-config-home for config file location.
## Protocol
Currently *all* mouse and keyboard events are sent via **UDP** for performance reasons.
Each event is sent as one single datagram, currently without any acknowledgement to guarantee 0% packet loss.
This means, any packet that is lost results in a discarded mouse / key event, which is ignored for now.
**UDP** also has the additional benefit that no reconnection logic is required.
Any client can just go offline and it will simply start working again as soon as it comes back online.
## Detailed OS Support
Additionally a tcp server is hosted for data that needs to be sent reliably (e.g. the keymap from the server or clipboard contents in the future) can be requested via a tcp connection.
In order to use a device for sending events, an **input-capture** backend is required, while receiving events requires
a supported **input-emulation** *and* **input-capture** backend.
## Bandwidth considerations
The most bandwidth is taken up by mouse events. A typical office mouse has a polling rate of 125Hz
while gaming mice typically have a much higher polling rate of 1000Hz.
A mouse Event consists of 21 Bytes:
- 1 Byte for the event type enum,
- 4 Bytes (u32) for the timestamp,
- 8 Bytes (f64) for dx,
- 8 Bytes (f64) for dy.
A suitable backend is chosen automatically based on the active desktop environment / compositor.
Additionally the IP header with 20 Bytes and the udp header with 8 Bytes take up another 28 Byte.
So in total there is 49 * 1000 Bytes/s for a 1000Hz gaming mouse.
This makes for a bandwidth requirement of 392 kbit/s in total _even_ for a high end gaming mouse.
So bandwidth is a non-issue.
The following sections detail the emulation and capture backends provided by lan-mouse and their support in desktop environments / operating systems.
Larger data chunks, like the keymap are offered by the server via tcp listening on the same port.
This way we dont need to implement any congestion control and leave this up to tcp.
In the future this can be used for e.g. clipboard contents as well.
### Input Emulation Support
## Packets per Second
While on LAN the performance is great,
some WIFI cards seem to struggle with the amount of packets per second,
particularly on high-end gaming mice with 1000Hz+ polling rates.
| Desktop / Backend | layer-shell | libei | windows | macos | x11 |
|---------------------------|--------------------------|--------------------------|--------------------------|----------------------------------------|-----|
| Wayland (wlroots) | :heavy_check_mark: | | | | |
| Wayland (KDE) | :heavy_check_mark: | :heavy_check_mark: | | | |
| Wayland (Gnome) | | :heavy_check_mark: | | | |
| Windows | | | :heavy_check_mark: | | |
| MacOS | | | | :heavy_check_mark: | |
| X11 | | | | | WIP |
- `wlroots`: This backend makes use of the [wlr-virtual-pointer-unstable-v1](https://wayland.app/protocols/wlr-virtual-pointer-unstable-v1) and [virtual-keyboard-unstable-v1](https://wayland.app/protocols/virtual-keyboard-unstable-v1) protocols and is supported by most wlroots based compositors.
- `libei`: This backend uses [libei](https://gitlab.freedesktop.org/libinput/libei) and is supported by GNOME >= 45 or KDE Plasma >= 6.1.
- `xdp`: This backend uses the [freedesktop remote-desktop-portal](https://flatpak.github.io/xdg-desktop-portal/#gdbus-org.freedesktop.portal.RemoteDesktop) and is supported on GNOME and Plasma.
- `x11`: Backend for X11 sessions.
- `windows`: Backend for Windows.
- `macos`: Backend for MacOS.
### Input Capture Support
| Desktop / Backend | wlroots | libei | remote-desktop portal | windows | macos | x11 |
|---------------------------|--------------------------|--------------------------|--------------------------|--------------------------|----------------------------------------|--------------------|
| Wayland (wlroots) | :heavy_check_mark: | | | | | |
| Wayland (KDE) | | :heavy_check_mark: | :heavy_check_mark: | | | |
| Wayland (Gnome) | | :heavy_check_mark: | :heavy_check_mark: | | | |
| Windows | | | | :heavy_check_mark: | | |
| MacOS | | | | | :heavy_check_mark: | |
| X11 | | | | | | :heavy_check_mark: |
- `layer-shell`: This backend creates a single pixel wide window on the edges of Displays to capture the cursor using the [layer-shell protocol](https://wayland.app/protocols/wlr-layer-shell-unstable-v1).
- `libei`: This backend uses [libei](https://gitlab.freedesktop.org/libinput/libei) and is supported by GNOME >= 45 or KDE Plasma >= 6.1.
- `windows`: Backend for input capture on Windows.
- `macos`: Backend for input capture on MacOS.
- `x11`: TODO (not yet supported)
The plan is to implement a way of accumulating packets and sending them as
one single key event to reduce the packet rate (basically reducing the polling
rate artificially).
The way movement data is currently sent is also quite wasteful since even a 16bit integer
is likely enough to represent even the fastest possible mouse movement.
A different encoding that is more efficient for smaller values like
[Protocol Buffers](https://protobuf.dev/programming-guides/encoding/)
would be a better choice for the future and could also help for WIFI connections.
## Security
Sending key and mouse event data over the local network might not be the biggest security concern but in any public network or business environment it's *QUITE* a problem to basically broadcast your keystrokes.
- There should be an encryption layer below the application to enable a secure link.
- The encryption keys could be generated by the graphical frontend.

15
build.rs Normal file
View File

@@ -0,0 +1,15 @@
use std::process::Command;
fn main() {
// commit hash
let git_describe = Command::new("git")
.arg("describe")
.arg("--always")
.arg("--dirty")
.arg("--tags")
.output()
.unwrap();
let git_describe = String::from_utf8(git_describe.stdout).unwrap();
println!("cargo::rustc-env=GIT_DESCRIBE={git_describe}");
}

View File

@@ -1,20 +1,28 @@
# example configuration
# capture_backend = "LayerShell"
# release bind
release_bind = [ "KeyA", "KeyS", "KeyD", "KeyF" ]
# optional port (defaults to 4242)
port = 4242
# 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"
hostname = "iridium"
# optional list of (known) ip addresses
ips = ["192.168.178.141"]
# optional port (defaults to 4242)
port = 4242
ips = ["192.168.178.156"]
# define a client on the left side with IP address 192.168.178.189
[left]
# The hostname is optional: When no hostname is specified,
# at least one ip address needs to be specified.
host_name = "thorium"
ips = ["192.168.178.189"]
hostname = "thorium"
# ips for ethernet and wifi
ips = ["192.168.178.189", "192.168.178.172"]
# optional port
port = 4242

View File

@@ -0,0 +1,12 @@
[Desktop Entry]
Categories=Utility;
Comment[en_US]=Mouse & Keyboard sharing via LAN
Comment=Mouse & Keyboard sharing via LAN
Comment[de_DE]=Maus- und Tastaturfreigabe über LAN
Exec=lan-mouse
Icon=de.feschber.LanMouse
Name[en_US]=Lan Mouse
Name=Lan Mouse
StartupNotify=true
Terminal=false
Type=Application

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

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

3
default.nix Normal file
View File

@@ -0,0 +1,3 @@
{ pkgs ? import <nixpkgs> { }
}:
pkgs.callPackage nix/default.nix { }

8
firewall/lan-mouse.xml Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- for packaging: /usr/lib/firewalld/services/lan-mouse.xml -->
<!-- configure manually: /etc/firewalld/services/lan-mouse.xml -->
<service>
<short>LAN Mouse</short>
<description>mouse and keyboard sharing via LAN</description>
<port port="4242" protocol="udp"/>
</service>

48
flake.lock generated Normal file
View File

@@ -0,0 +1,48 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1728018373,
"narHash": "sha256-NOiTvBbRLIOe5F6RbHaAh6++BNjsb149fGZd1T4+KBg=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "bc947f541ae55e999ffdb4013441347d83b00feb",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay"
}
},
"rust-overlay": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1728181869,
"narHash": "sha256-sQXHXsjIcGEoIHkB+RO6BZdrPfB+43V1TEpyoWRI3ww=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "cd46aa3906c14790ef5cbe278d9e54f2c38f95c0",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

66
flake.nix Normal file
View File

@@ -0,0 +1,66 @@
{
description = "Nix Flake for lan-mouse";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
rust-overlay = {
url = "github:oxalica/rust-overlay";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = {
self,
nixpkgs,
rust-overlay,
...
}: let
inherit (nixpkgs) lib;
genSystems = lib.genAttrs [
"aarch64-darwin"
"aarch64-linux"
"x86_64-darwin"
"x86_64-linux"
];
pkgsFor = system:
import nixpkgs {
inherit system;
overlays = [
rust-overlay.overlays.default
];
};
mkRustToolchain = pkgs:
pkgs.rust-bin.stable.latest.default.override {
extensions = ["rust-src"];
};
pkgs = genSystems (system: import nixpkgs {inherit system;});
in {
packages = genSystems (system: rec {
default = pkgs.${system}.callPackage ./nix {};
lan-mouse = default;
});
homeManagerModules.default = import ./nix/hm-module.nix self;
devShells = genSystems (system: let
pkgs = pkgsFor system;
rust = mkRustToolchain pkgs;
in {
default = pkgs.mkShell {
packages = with pkgs; [
rust
rust-analyzer-unwrapped
pkg-config
xorg.libX11
gtk4
libadwaita
librsvg
xorg.libXtst
] ++ lib.optionals stdenv.isDarwin
(with darwin.apple_sdk_11_0.frameworks; [
CoreGraphics
ApplicationServices
]);
RUST_SRC_PATH = "${rust}/lib/rustlib/src/rust/library";
};
});
};
}

75
input-capture/Cargo.toml Normal file
View File

@@ -0,0 +1,75 @@
[package]
name = "input-capture"
description = "cross-platform input-capture library used by lan-mouse"
version = "0.3.0"
edition = "2021"
license = "GPL-3.0-or-later"
repository = "https://github.com/feschber/lan-mouse"
[dependencies]
futures = "0.3.28"
futures-core = "0.3.30"
log = "0.4.22"
input-event = { path = "../input-event", version = "0.3.0" }
memmap = "0.7"
tempfile = "3.8"
thiserror = "2.0.0"
tokio = { version = "1.32.0", features = [
"io-util",
"io-std",
"macros",
"net",
"process",
"rt",
"sync",
"signal",
] }
once_cell = "1.19.0"
async-trait = "0.1.81"
tokio-util = "0.7.11"
[target.'cfg(all(unix, not(target_os="macos")))'.dependencies]
wayland-client = { version = "0.31.1", optional = true }
wayland-protocols = { version = "0.32.1", features = [
"client",
"staging",
"unstable",
], optional = true }
wayland-protocols-wlr = { version = "0.3.1", features = [
"client",
], optional = true }
x11 = { version = "2.21.0", features = ["xlib", "xtest"], optional = true }
ashpd = { version = "0.10", default-features = false, features = [
"tokio",
], optional = true }
reis = { version = "0.4", features = ["tokio"], optional = true }
[target.'cfg(target_os="macos")'.dependencies]
core-graphics = { version = "0.24.0", features = ["highsierra"] }
core-foundation = "0.10.0"
core-foundation-sys = "0.8.6"
libc = "0.2.155"
keycode = "0.4.0"
bitflags = "2.6.0"
[target.'cfg(windows)'.dependencies]
windows = { version = "0.58.0", features = [
"Win32_System_LibraryLoader",
"Win32_System_Threading",
"Win32_Foundation",
"Win32_Graphics",
"Win32_Graphics_Gdi",
"Win32_UI_Input_KeyboardAndMouse",
"Win32_UI_WindowsAndMessaging",
] }
[features]
default = ["layer_shell", "x11", "libei"]
layer_shell = [
"dep:wayland-client",
"dep:wayland-protocols",
"dep:wayland-protocols-wlr",
]
x11 = ["dep:x11"]
libei = ["dep:reis", "dep:ashpd"]

View File

@@ -0,0 +1,86 @@
use std::f64::consts::PI;
use std::pin::Pin;
use std::task::{ready, Context, Poll};
use std::time::Duration;
use async_trait::async_trait;
use futures_core::Stream;
use input_event::PointerEvent;
use tokio::time::{self, Instant, Interval};
use super::{Capture, CaptureError, CaptureEvent, Position};
pub struct DummyInputCapture {
start: Option<Instant>,
interval: Interval,
offset: (i32, i32),
}
impl DummyInputCapture {
pub fn new() -> Self {
Self {
start: None,
interval: time::interval(Duration::from_millis(1)),
offset: (0, 0),
}
}
}
impl Default for DummyInputCapture {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl Capture for DummyInputCapture {
async fn create(&mut self, _pos: Position) -> Result<(), CaptureError> {
Ok(())
}
async fn destroy(&mut self, _pos: Position) -> Result<(), CaptureError> {
Ok(())
}
async fn release(&mut self) -> Result<(), CaptureError> {
Ok(())
}
async fn terminate(&mut self) -> Result<(), CaptureError> {
Ok(())
}
}
const FREQUENCY_HZ: f64 = 1.0;
const RADIUS: f64 = 100.0;
impl Stream for DummyInputCapture {
type Item = Result<(Position, CaptureEvent), CaptureError>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
let current = ready!(self.interval.poll_tick(cx));
let event = match self.start {
None => {
self.start.replace(current);
CaptureEvent::Begin
}
Some(start) => {
let elapsed = start.elapsed();
let elapsed_sec_f64 = elapsed.as_secs_f64();
let second_fraction = elapsed_sec_f64 - elapsed_sec_f64 as u64 as f64;
let radians = second_fraction * 2. * PI * FREQUENCY_HZ;
let offset = (radians.cos() * RADIUS * 2., (radians * 2.).sin() * RADIUS);
let offset = (offset.0 as i32, offset.1 as i32);
let relative_motion = (offset.0 - self.offset.0, offset.1 - self.offset.1);
self.offset = offset;
let (dx, dy) = (relative_motion.0 as f64, relative_motion.1 as f64);
CaptureEvent::Input(input_event::Event::Pointer(PointerEvent::Motion {
time: 0,
dx,
dy,
}))
}
};
Poll::Ready(Some(Ok((Position::Left, event))))
}
}

157
input-capture/src/error.rs Normal file
View File

@@ -0,0 +1,157 @@
use thiserror::Error;
#[derive(Debug, Error)]
pub enum InputCaptureError {
#[error("error creating input-capture: `{0}`")]
Create(#[from] CaptureCreationError),
#[error("error while capturing input: `{0}`")]
Capture(#[from] CaptureError),
}
#[cfg(all(unix, feature = "layer_shell", not(target_os = "macos")))]
use std::io;
#[cfg(all(unix, feature = "layer_shell", not(target_os = "macos")))]
use wayland_client::{
backend::WaylandError,
globals::{BindError, GlobalError},
ConnectError, DispatchError,
};
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
use ashpd::desktop::ResponseError;
#[cfg(target_os = "macos")]
use core_graphics::base::CGError;
#[derive(Debug, Error)]
pub enum CaptureError {
#[error("activation stream closed unexpectedly")]
ActivationClosed,
#[error("libei stream was closed")]
EndOfStream,
#[error("io error: `{0}`")]
Io(#[from] std::io::Error),
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
#[error("libei error: `{0}`")]
Reis(#[from] reis::Error),
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
#[error(transparent)]
Portal(#[from] ashpd::Error),
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
#[error("libei disconnected - reason: `{0}`")]
Disconnected(String),
#[cfg(target_os = "macos")]
#[error("failed to warp mouse cursor: `{0}`")]
WarpCursor(CGError),
#[cfg(target_os = "macos")]
#[error("reset_mouse_position called without a connected client")]
ResetMouseWithoutClient,
#[cfg(target_os = "macos")]
#[error("core-graphics error: {0}")]
CoreGraphics(CGError),
#[cfg(target_os = "macos")]
#[error("unable to map key event: {0}")]
KeyMapError(i64),
#[cfg(target_os = "macos")]
#[error("Event tap disabled")]
EventTapDisabled,
}
#[derive(Debug, Error)]
pub enum CaptureCreationError {
#[error("no backend available")]
NoAvailableBackend,
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
#[error("error creating input-capture-portal backend: `{0}`")]
Libei(#[from] LibeiCaptureCreationError),
#[cfg(all(unix, feature = "layer_shell", not(target_os = "macos")))]
#[error("error creating layer-shell capture backend: `{0}`")]
LayerShell(#[from] LayerShellCaptureCreationError),
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
#[error("error creating x11 capture backend: `{0}`")]
X11(#[from] X11InputCaptureCreationError),
#[cfg(windows)]
#[error("error creating windows capture backend")]
Windows,
#[cfg(target_os = "macos")]
#[error("error creating macos capture backend: `{0}`")]
MacOS(#[from] MacosCaptureCreationError),
}
impl CaptureCreationError {
/// request was intentionally denied by the user
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
pub(crate) fn cancelled_by_user(&self) -> bool {
matches!(
self,
CaptureCreationError::Libei(LibeiCaptureCreationError::Ashpd(ashpd::Error::Response(
ResponseError::Cancelled
)))
)
}
#[cfg(not(all(unix, feature = "libei", not(target_os = "macos"))))]
pub(crate) fn cancelled_by_user(&self) -> bool {
false
}
}
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
#[derive(Debug, Error)]
pub enum LibeiCaptureCreationError {
#[error("xdg-desktop-portal: `{0}`")]
Ashpd(#[from] ashpd::Error),
}
#[cfg(all(unix, feature = "layer_shell", not(target_os = "macos")))]
#[derive(Debug, Error)]
#[error("{protocol} protocol not supported: {inner}")]
pub struct WaylandBindError {
inner: BindError,
protocol: &'static str,
}
#[cfg(all(unix, feature = "layer_shell", not(target_os = "macos")))]
impl WaylandBindError {
pub(crate) fn new(inner: BindError, protocol: &'static str) -> Self {
Self { inner, protocol }
}
}
#[cfg(all(unix, feature = "layer_shell", not(target_os = "macos")))]
#[derive(Debug, Error)]
pub enum LayerShellCaptureCreationError {
#[error(transparent)]
Connect(#[from] ConnectError),
#[error(transparent)]
Global(#[from] GlobalError),
#[error(transparent)]
Wayland(#[from] WaylandError),
#[error(transparent)]
Bind(#[from] WaylandBindError),
#[error(transparent)]
Dispatch(#[from] DispatchError),
#[error(transparent)]
Io(#[from] io::Error),
}
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
#[derive(Debug, Error)]
pub enum X11InputCaptureCreationError {
#[error("X11 input capture is not yet implemented :(")]
NotImplemented,
}
#[cfg(target_os = "macos")]
#[derive(Debug, Error)]
pub enum MacosCaptureCreationError {
#[error("event source creation failed!")]
EventSourceCreation,
#[cfg(target_os = "macos")]
#[error("event tap creation failed")]
EventTapCreation,
#[error("failed to set CG Cursor property")]
CGCursorProperty,
#[cfg(target_os = "macos")]
#[error("failed to get display ids: {0}")]
ActiveDisplays(CGError),
}

View File

@@ -0,0 +1,982 @@
use async_trait::async_trait;
use futures_core::Stream;
use std::{
collections::VecDeque,
env,
io::{self, ErrorKind},
os::fd::{AsFd, RawFd},
pin::Pin,
task::{ready, Context, Poll},
};
use tokio::io::unix::AsyncFd;
use std::{
fs::File,
io::{BufWriter, Write},
os::unix::prelude::AsRawFd,
sync::Arc,
};
use wayland_protocols::{
wp::{
keyboard_shortcuts_inhibit::zv1::client::{
zwp_keyboard_shortcuts_inhibit_manager_v1::ZwpKeyboardShortcutsInhibitManagerV1,
zwp_keyboard_shortcuts_inhibitor_v1::ZwpKeyboardShortcutsInhibitorV1,
},
pointer_constraints::zv1::client::{
zwp_locked_pointer_v1::ZwpLockedPointerV1,
zwp_pointer_constraints_v1::{Lifetime, ZwpPointerConstraintsV1},
},
relative_pointer::zv1::client::{
zwp_relative_pointer_manager_v1::ZwpRelativePointerManagerV1,
zwp_relative_pointer_v1::{self, ZwpRelativePointerV1},
},
},
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},
zwlr_layer_surface_v1::{self, Anchor, KeyboardInteractivity, ZwlrLayerSurfaceV1},
};
use wayland_client::{
backend::{ReadEventsGuard, WaylandError},
delegate_noop,
globals::{registry_queue_init, GlobalListContents},
protocol::{
wl_buffer, wl_compositor,
wl_keyboard::{self, WlKeyboard},
wl_output::{self, WlOutput},
wl_pointer::{self, WlPointer},
wl_region, wl_registry, wl_seat, wl_shm, wl_shm_pool,
wl_surface::WlSurface,
},
Connection, Dispatch, DispatchError, EventQueue, QueueHandle, WEnum,
};
use input_event::{Event, KeyboardEvent, PointerEvent};
use crate::{CaptureError, CaptureEvent};
use super::{
error::{LayerShellCaptureCreationError, WaylandBindError},
Capture, Position,
};
struct Globals {
compositor: wl_compositor::WlCompositor,
pointer_constraints: ZwpPointerConstraintsV1,
relative_pointer_manager: ZwpRelativePointerManagerV1,
shortcut_inhibit_manager: Option<ZwpKeyboardShortcutsInhibitManagerV1>,
seat: wl_seat::WlSeat,
shm: wl_shm::WlShm,
layer_shell: ZwlrLayerShellV1,
outputs: Vec<WlOutput>,
xdg_output_manager: ZxdgOutputManagerV1,
}
#[derive(Debug, Clone)]
struct OutputInfo {
name: String,
position: (i32, i32),
size: (i32, i32),
}
impl OutputInfo {
fn new() -> Self {
Self {
name: "".to_string(),
position: (0, 0),
size: (0, 0),
}
}
}
struct State {
pointer: Option<WlPointer>,
keyboard: Option<WlKeyboard>,
pointer_lock: Option<ZwpLockedPointerV1>,
rel_pointer: Option<ZwpRelativePointerV1>,
shortcut_inhibitor: Option<ZwpKeyboardShortcutsInhibitorV1>,
active_windows: Vec<Arc<Window>>,
focused: Option<Arc<Window>>,
g: Globals,
wayland_fd: RawFd,
read_guard: Option<ReadEventsGuard>,
qh: QueueHandle<Self>,
pending_events: VecDeque<(Position, CaptureEvent)>,
output_info: Vec<(WlOutput, OutputInfo)>,
scroll_discrete_pending: bool,
}
struct Inner {
state: State,
queue: EventQueue<State>,
}
impl AsRawFd for Inner {
fn as_raw_fd(&self) -> RawFd {
self.state.wayland_fd
}
}
pub struct LayerShellInputCapture(AsyncFd<Inner>);
struct Window {
buffer: wl_buffer::WlBuffer,
surface: WlSurface,
layer_surface: ZwlrLayerSurfaceV1,
pos: Position,
}
impl Window {
fn new(
state: &State,
qh: &QueueHandle<State>,
output: &WlOutput,
pos: Position,
size: (i32, i32),
) -> Window {
log::debug!("creating window output: {output:?}, size: {size:?}");
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
.shm
.create_pool(file.as_fd(), (width * height * 4) as i32, qh, ());
let buffer = pool.create_buffer(
0,
width as i32,
height as i32,
(width * 4) as i32,
wl_shm::Format::Argb8888,
qh,
(),
);
let surface = g.compositor.create_surface(qh, ());
let layer_surface = g.layer_shell.get_layer_surface(
&surface,
Some(output),
Layer::Overlay,
"LAN Mouse Sharing".into(),
qh,
(),
);
let anchor = match pos {
Position::Left => Anchor::Left,
Position::Right => Anchor::Right,
Position::Top => Anchor::Top,
Position::Bottom => Anchor::Bottom,
};
layer_surface.set_anchor(anchor);
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 {
pos,
buffer,
surface,
layer_surface,
}
}
}
impl Drop for Window {
fn drop(&mut self) {
log::debug!("destroying window!");
self.layer_surface.destroy();
self.surface.destroy();
self.buffer.destroy();
}
}
fn get_edges(outputs: &[(WlOutput, OutputInfo)], pos: Position) -> Vec<(WlOutput, i32)> {
outputs
.iter()
.map(|(o, i)| {
(
o.clone(),
match pos {
Position::Left => i.position.0,
Position::Right => i.position.0 + i.size.0,
Position::Top => i.position.1,
Position::Bottom => i.position.1 + i.size.1,
},
)
})
.collect()
}
fn get_output_configuration(state: &State, pos: Position) -> Vec<(WlOutput, OutputInfo)> {
// get all output edges corresponding to the position
let edges = get_edges(&state.output_info, pos);
log::debug!("edges: {edges:?}");
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).any(|e| &e == edge))
.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 {
if env::var("LM_DEBUG_LAYER_SHELL").ok().is_some() {
// AARRGGBB
buf.write_all(&0xff11d116u32.to_ne_bytes()).unwrap();
} else {
// AARRGGBB
buf.write_all(&0x00000000u32.to_ne_bytes()).unwrap();
}
}
}
}
impl LayerShellInputCapture {
pub fn new() -> std::result::Result<Self, LayerShellCaptureCreationError> {
let conn = Connection::connect_to_env()?;
let (g, mut queue) = registry_queue_init::<State>(&conn)?;
let qh = queue.handle();
let compositor: wl_compositor::WlCompositor = g
.bind(&qh, 4..=5, ())
.map_err(|e| WaylandBindError::new(e, "wl_compositor 4..=5"))?;
let xdg_output_manager: ZxdgOutputManagerV1 = g
.bind(&qh, 1..=3, ())
.map_err(|e| WaylandBindError::new(e, "xdg_output_manager 1..=3"))?;
let shm: wl_shm::WlShm = g
.bind(&qh, 1..=1, ())
.map_err(|e| WaylandBindError::new(e, "wl_shm"))?;
let layer_shell: ZwlrLayerShellV1 = g
.bind(&qh, 3..=4, ())
.map_err(|e| WaylandBindError::new(e, "wlr_layer_shell 3..=4"))?;
let seat: wl_seat::WlSeat = g
.bind(&qh, 7..=8, ())
.map_err(|e| WaylandBindError::new(e, "wl_seat 7..=8"))?;
let pointer_constraints: ZwpPointerConstraintsV1 = g
.bind(&qh, 1..=1, ())
.map_err(|e| WaylandBindError::new(e, "zwp_pointer_constraints_v1"))?;
let relative_pointer_manager: ZwpRelativePointerManagerV1 = g
.bind(&qh, 1..=1, ())
.map_err(|e| WaylandBindError::new(e, "zwp_relative_pointer_manager_v1"))?;
let shortcut_inhibit_manager: Result<
ZwpKeyboardShortcutsInhibitManagerV1,
WaylandBindError,
> = g
.bind(&qh, 1..=1, ())
.map_err(|e| WaylandBindError::new(e, "zwp_keyboard_shortcuts_inhibit_manager_v1"));
// layer-shell backend still works without this protocol so we make it an optional dependency
if let Err(e) = &shortcut_inhibit_manager {
log::warn!("shortcut_inhibit_manager not supported: {e}\nkeybinds handled by the compositor will not be passed
to the client");
}
let shortcut_inhibit_manager = shortcut_inhibit_manager.ok();
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()?;
let wayland_fd = queue.as_fd().as_raw_fd();
let mut state = State {
pointer: None,
keyboard: None,
g,
pointer_lock: None,
rel_pointer: None,
shortcut_inhibitor: None,
active_windows: Vec::new(),
focused: None,
qh,
wayland_fd,
read_guard: None,
pending_events: VecDeque::new(),
output_info: vec![],
scroll_discrete_pending: false,
};
// 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 = loop {
match queue.prepare_read() {
Some(r) => break r,
None => {
queue.dispatch_pending(&mut state)?;
continue;
}
}
};
state.read_guard = Some(read_guard);
let inner = AsyncFd::new(Inner { queue, state })?;
Ok(LayerShellInputCapture(inner))
}
fn add_client(&mut self, pos: Position) {
self.0.get_mut().state.add_client(pos);
}
fn delete_client(&mut self, pos: Position) {
let inner = self.0.get_mut();
// remove all windows corresponding to this client
while let Some(i) = inner.state.active_windows.iter().position(|w| w.pos == pos) {
inner.state.active_windows.remove(i);
inner.state.focused = None;
}
}
}
impl State {
fn grab(
&mut self,
surface: &WlSurface,
pointer: &WlPointer,
serial: u32,
qh: &QueueHandle<State>,
) {
let window = self.focused.as_ref().unwrap();
// hide the cursor
pointer.set_cursor(serial, None, 0, 0);
// capture input
window
.layer_surface
.set_keyboard_interactivity(KeyboardInteractivity::Exclusive);
window.surface.commit();
// lock pointer
if self.pointer_lock.is_none() {
self.pointer_lock = Some(self.g.pointer_constraints.lock_pointer(
surface,
pointer,
None,
Lifetime::Persistent,
qh,
(),
));
}
// request relative input
if self.rel_pointer.is_none() {
self.rel_pointer = Some(self.g.relative_pointer_manager.get_relative_pointer(
pointer,
qh,
(),
));
}
// capture modifier keys
if let Some(shortcut_inhibit_manager) = &self.g.shortcut_inhibit_manager {
if self.shortcut_inhibitor.is_none() {
self.shortcut_inhibitor =
Some(shortcut_inhibit_manager.inhibit_shortcuts(surface, &self.g.seat, qh, ()));
}
}
}
fn ungrab(&mut self) {
// get focused client
let window = match self.focused.as_ref() {
Some(focused) => focused,
None => return,
};
// ungrab surface
window
.layer_surface
.set_keyboard_interactivity(KeyboardInteractivity::None);
window.surface.commit();
// destroy pointer lock
if let Some(pointer_lock) = &self.pointer_lock {
pointer_lock.destroy();
self.pointer_lock = None;
}
// destroy relative input
if let Some(rel_pointer) = &self.rel_pointer {
rel_pointer.destroy();
self.rel_pointer = None;
}
// destroy shortcut inhibitor
if let Some(shortcut_inhibitor) = &self.shortcut_inhibitor {
shortcut_inhibitor.destroy();
self.shortcut_inhibitor = None;
}
}
fn add_client(&mut self, pos: Position) {
let outputs = get_output_configuration(self, pos);
log::debug!("outputs: {outputs:?}");
outputs.iter().for_each(|(o, i)| {
let window = Window::new(self, &self.qh, o, pos, i.size);
let window = Arc::new(window);
self.active_windows.push(window);
});
}
fn update_windows(&mut self) {
log::debug!("updating windows");
log::debug!("output info: {:?}", self.output_info);
let clients: Vec<_> = self.active_windows.drain(..).map(|w| w.pos).collect();
for pos in clients {
self.add_client(pos);
}
}
}
impl Inner {
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) -> io::Result<()> {
loop {
match self.queue.prepare_read() {
None => match self.queue.dispatch_pending(&mut self.state) {
Ok(_) => continue,
Err(DispatchError::Backend(WaylandError::Io(e))) => return Err(e),
Err(e) => panic!("failed to dispatch wayland events: {e}"),
},
Some(r) => {
self.state.read_guard = Some(r);
break Ok(());
}
}
}
}
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) -> io::Result<()> {
// flush outgoing events
match self.queue.flush() {
Ok(_) => (),
Err(e) => match e {
WaylandError::Io(e) => {
return Err(e);
}
WaylandError::Protocol(e) => {
panic!("wayland protocol violation: {e}")
}
},
}
Ok(())
}
}
#[async_trait]
impl Capture for LayerShellInputCapture {
async fn create(&mut self, pos: Position) -> Result<(), CaptureError> {
self.add_client(pos);
let inner = self.0.get_mut();
Ok(inner.flush_events()?)
}
async fn destroy(&mut self, pos: Position) -> Result<(), CaptureError> {
self.delete_client(pos);
let inner = self.0.get_mut();
Ok(inner.flush_events()?)
}
async fn release(&mut self) -> Result<(), CaptureError> {
log::debug!("releasing pointer");
let inner = self.0.get_mut();
inner.state.ungrab();
Ok(inner.flush_events()?)
}
async fn terminate(&mut self) -> Result<(), CaptureError> {
Ok(())
}
}
impl Stream for LayerShellInputCapture {
type Item = Result<(Position, CaptureEvent), CaptureError>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
if let Some(event) = self.0.get_mut().state.pending_events.pop_front() {
return Poll::Ready(Some(Ok(event)));
}
loop {
let mut guard = ready!(self.0.poll_read_ready_mut(cx))?;
{
let inner = guard.get_inner_mut();
// read events
while inner.read() {
// prepare next read
match inner.prepare_read() {
Ok(_) => {}
Err(e) => return Poll::Ready(Some(Err(e.into()))),
}
}
// dispatch the events
inner.dispatch_events();
// flush outgoing events
if let Err(e) = inner.flush_events() {
if e.kind() != ErrorKind::WouldBlock {
return Poll::Ready(Some(Err(e.into())));
}
}
// prepare for the next read
match inner.prepare_read() {
Ok(_) => {}
Err(e) => return Poll::Ready(Some(Err(e.into()))),
}
}
// clear read readiness for tokio read guard
// guard.clear_ready_matching(Ready::READABLE);
guard.clear_ready();
// if an event has been queued during dispatch_events() we return it
match guard.get_inner_mut().state.pending_events.pop_front() {
Some(event) => return Poll::Ready(Some(Ok(event))),
None => continue,
}
}
}
}
impl Dispatch<wl_seat::WlSeat, ()> for State {
fn event(
state: &mut Self,
seat: &wl_seat::WlSeat,
event: <wl_seat::WlSeat as wayland_client::Proxy>::Event,
_: &(),
_: &Connection,
qh: &QueueHandle<Self>,
) {
if let wl_seat::Event::Capabilities {
capabilities: WEnum::Value(capabilities),
} = event
{
if capabilities.contains(wl_seat::Capability::Pointer) {
if let Some(p) = state.pointer.take() {
p.release();
}
state.pointer.replace(seat.get_pointer(qh, ()));
}
if capabilities.contains(wl_seat::Capability::Keyboard) {
if let Some(k) = state.keyboard.take() {
k.release();
}
seat.get_keyboard(qh, ());
}
}
}
}
impl Dispatch<WlPointer, ()> for State {
fn event(
app: &mut Self,
pointer: &WlPointer,
event: <WlPointer as wayland_client::Proxy>::Event,
_: &(),
_: &Connection,
qh: &QueueHandle<Self>,
) {
match event {
wl_pointer::Event::Enter {
serial,
surface,
surface_x: _,
surface_y: _,
} => {
// get client corresponding to the focused surface
{
if let Some(window) = app.active_windows.iter().find(|w| w.surface == surface) {
app.focused = Some(window.clone());
app.grab(&surface, pointer, serial, qh);
} else {
return;
}
}
let pos = app
.active_windows
.iter()
.find(|w| w.surface == surface)
.map(|w| w.pos)
.unwrap();
app.pending_events.push_back((pos, CaptureEvent::Begin));
}
wl_pointer::Event::Leave { .. } => {
/* There are rare cases, where when a window is opened in
* just the wrong moment, the pointer is released, while
* still grabbed.
* In that case, the pointer must be ungrabbed, otherwise
* it is impossible to grab it again (since the pointer
* lock, relative pointer,... objects are still in place)
*/
if app.pointer_lock.is_some() {
log::warn!("compositor released mouse");
}
app.ungrab();
}
wl_pointer::Event::Button {
serial: _,
time,
button,
state,
} => {
let window = app.focused.as_ref().unwrap();
app.pending_events.push_back((
window.pos,
CaptureEvent::Input(Event::Pointer(PointerEvent::Button {
time,
button,
state: u32::from(state),
})),
));
}
wl_pointer::Event::Axis { time, axis, value } => {
let window = app.focused.as_ref().unwrap();
if app.scroll_discrete_pending {
// each axisvalue120 event is coupled with
// a corresponding axis event, which needs to
// be ignored to not duplicate the scrolling
app.scroll_discrete_pending = false;
} else {
app.pending_events.push_back((
window.pos,
CaptureEvent::Input(Event::Pointer(PointerEvent::Axis {
time,
axis: u32::from(axis) as u8,
value,
})),
));
}
}
wl_pointer::Event::AxisValue120 { axis, value120 } => {
let window = app.focused.as_ref().unwrap();
app.scroll_discrete_pending = true;
app.pending_events.push_back((
window.pos,
CaptureEvent::Input(Event::Pointer(PointerEvent::AxisDiscrete120 {
axis: u32::from(axis) as u8,
value: value120,
})),
));
}
wl_pointer::Event::Frame {} => {
// TODO properly handle frame events
// we simply insert a frame event on the client side
// after each event for now
}
_ => {}
}
}
}
impl Dispatch<WlKeyboard, ()> for State {
fn event(
app: &mut Self,
_: &WlKeyboard,
event: wl_keyboard::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
let window = &app.focused;
match event {
wl_keyboard::Event::Key {
serial: _,
time,
key,
state,
} => {
if let Some(window) = window {
app.pending_events.push_back((
window.pos,
CaptureEvent::Input(Event::Keyboard(KeyboardEvent::Key {
time,
key,
state: u32::from(state) as u8,
})),
));
}
}
wl_keyboard::Event::Modifiers {
serial: _,
mods_depressed,
mods_latched,
mods_locked,
group,
} => {
if let Some(window) = window {
app.pending_events.push_back((
window.pos,
CaptureEvent::Input(Event::Keyboard(KeyboardEvent::Modifiers {
depressed: mods_depressed,
latched: mods_latched,
locked: mods_locked,
group,
})),
));
}
}
_ => (),
}
}
}
impl Dispatch<ZwpRelativePointerV1, ()> for State {
fn event(
app: &mut Self,
_: &ZwpRelativePointerV1,
event: <ZwpRelativePointerV1 as wayland_client::Proxy>::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
if let zwp_relative_pointer_v1::Event::RelativeMotion {
utime_hi,
utime_lo,
dx_unaccel: dx,
dy_unaccel: dy,
..
} = event
{
if let Some(window) = &app.focused {
let time = (((utime_hi as u64) << 32 | utime_lo as u64) / 1000) as u32;
app.pending_events.push_back((
window.pos,
CaptureEvent::Input(Event::Pointer(PointerEvent::Motion { time, dx, dy })),
));
}
}
}
}
impl Dispatch<ZwlrLayerSurfaceV1, ()> for State {
fn event(
app: &mut Self,
layer_surface: &ZwlrLayerSurfaceV1,
event: <ZwlrLayerSurfaceV1 as wayland_client::Proxy>::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
if let zwlr_layer_surface_v1::Event::Configure { serial, .. } = event {
if let Some(window) = app
.active_windows
.iter()
.find(|w| &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
impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for State {
fn event(
_state: &mut Self,
_proxy: &wl_registry::WlRegistry,
_event: <wl_registry::WlRegistry as wayland_client::Proxy>::Event,
_data: &GlobalListContents,
_conn: &Connection,
_qhandle: &QueueHandle<Self>,
) {
}
}
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: _,
} => {
if interface.as_str() == "wl_output" {
log::debug!("wl_output global");
state
.g
.outputs
.push(registry.bind::<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 { .. } => {}
_ => {}
}
}
}
impl Dispatch<WlOutput, ()> for State {
fn event(
state: &mut Self,
_proxy: &WlOutput,
event: <WlOutput as wayland_client::Proxy>::Event,
_data: &(),
_conn: &Connection,
_qhandle: &QueueHandle<Self>,
) {
if let wl_output::Event::Done = event {
state.update_windows();
}
}
}
// don't emit any events
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!(State: ignore ZxdgOutputManagerV1);
delegate_noop!(State: ignore wl_shm::WlShm);
delegate_noop!(State: ignore wl_buffer::WlBuffer);
delegate_noop!(State: ignore WlSurface);
delegate_noop!(State: ignore ZwpKeyboardShortcutsInhibitorV1);
delegate_noop!(State: ignore ZwpLockedPointerV1);

338
input-capture/src/lib.rs Normal file
View File

@@ -0,0 +1,338 @@
use std::{
collections::{HashMap, HashSet, VecDeque},
fmt::Display,
mem::swap,
task::{ready, Poll},
};
use async_trait::async_trait;
use futures::StreamExt;
use futures_core::Stream;
use input_event::{scancode, Event, KeyboardEvent};
pub use error::{CaptureCreationError, CaptureError, InputCaptureError};
pub mod error;
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
mod libei;
#[cfg(target_os = "macos")]
mod macos;
#[cfg(all(unix, feature = "layer_shell", not(target_os = "macos")))]
mod layer_shell;
#[cfg(windows)]
mod windows;
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
mod x11;
/// fallback input capture (does not produce events)
mod dummy;
pub type CaptureHandle = u64;
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum CaptureEvent {
/// capture on this capture handle is now active
Begin,
/// input event coming from capture handle
Input(Event),
}
impl Display for CaptureEvent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CaptureEvent::Begin => write!(f, "begin capture"),
CaptureEvent::Input(e) => write!(f, "{e}"),
}
}
}
#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)]
pub enum Position {
Left,
Right,
Top,
Bottom,
}
impl Position {
pub fn opposite(&self) -> Self {
match self {
Position::Left => Self::Right,
Position::Right => Self::Left,
Position::Top => Self::Bottom,
Position::Bottom => Self::Top,
}
}
}
impl Display for Position {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let pos = match self {
Position::Left => "left",
Position::Right => "right",
Position::Top => "top",
Position::Bottom => "bottom",
};
write!(f, "{}", pos)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Backend {
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
InputCapturePortal,
#[cfg(all(unix, feature = "layer_shell", not(target_os = "macos")))]
LayerShell,
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
X11,
#[cfg(windows)]
Windows,
#[cfg(target_os = "macos")]
MacOs,
Dummy,
}
impl Display for Backend {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
Backend::InputCapturePortal => write!(f, "input-capture-portal"),
#[cfg(all(unix, feature = "layer_shell", not(target_os = "macos")))]
Backend::LayerShell => write!(f, "layer-shell"),
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
Backend::X11 => write!(f, "X11"),
#[cfg(windows)]
Backend::Windows => write!(f, "windows"),
#[cfg(target_os = "macos")]
Backend::MacOs => write!(f, "MacOS"),
Backend::Dummy => write!(f, "dummy"),
}
}
}
pub struct InputCapture {
/// capture backend
capture: Box<dyn Capture>,
/// keys pressed by active capture
pressed_keys: HashSet<scancode::Linux>,
/// map from position to ids
position_map: HashMap<Position, Vec<CaptureHandle>>,
/// map from id to position
id_map: HashMap<CaptureHandle, Position>,
/// pending events
pending: VecDeque<(CaptureHandle, CaptureEvent)>,
}
impl InputCapture {
/// create a new client with the given id
pub async fn create(&mut self, id: CaptureHandle, pos: Position) -> Result<(), CaptureError> {
assert!(!self.id_map.contains_key(&id));
self.id_map.insert(id, pos);
if let Some(v) = self.position_map.get_mut(&pos) {
v.push(id);
Ok(())
} else {
self.position_map.insert(pos, vec![id]);
self.capture.create(pos).await
}
}
/// destroy the client with the given id, if it exists
pub async fn destroy(&mut self, id: CaptureHandle) -> Result<(), CaptureError> {
let pos = self
.id_map
.remove(&id)
.expect("no position for this handle");
log::debug!("destroying capture {id} @ {pos}");
let remaining = self.position_map.get_mut(&pos).expect("id vector");
remaining.retain(|&i| i != id);
log::debug!("remaining ids @ {pos}: {remaining:?}");
if remaining.is_empty() {
log::debug!("destroying capture @ {pos} - no remaining ids");
self.position_map.remove(&pos);
self.capture.destroy(pos).await?;
}
Ok(())
}
/// release mouse
pub async fn release(&mut self) -> Result<(), CaptureError> {
self.pressed_keys.clear();
self.capture.release().await
}
/// destroy the input capture
pub async fn terminate(&mut self) -> Result<(), CaptureError> {
self.capture.terminate().await
}
/// creates a new [`InputCapture`]
pub async fn new(backend: Option<Backend>) -> Result<Self, CaptureCreationError> {
let capture = create(backend).await?;
Ok(Self {
capture,
id_map: Default::default(),
pending: Default::default(),
position_map: Default::default(),
pressed_keys: HashSet::new(),
})
}
/// check whether the given keys are pressed
pub fn keys_pressed(&self, keys: &[scancode::Linux]) -> bool {
keys.iter().all(|k| self.pressed_keys.contains(k))
}
fn update_pressed_keys(&mut self, key: u32, state: u8) {
if let Ok(scancode) = scancode::Linux::try_from(key) {
log::debug!("key: {key}, state: {state}, scancode: {scancode:?}");
match state {
1 => self.pressed_keys.insert(scancode),
_ => self.pressed_keys.remove(&scancode),
};
}
}
}
impl Stream for InputCapture {
type Item = Result<(CaptureHandle, CaptureEvent), CaptureError>;
fn poll_next(
mut self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> Poll<Option<Self::Item>> {
if let Some(e) = self.pending.pop_front() {
return Poll::Ready(Some(Ok(e)));
}
// ready
let event = ready!(self.capture.poll_next_unpin(cx));
// stream closed
let event = match event {
Some(e) => e,
None => return Poll::Ready(None),
};
// error occurred
let (pos, event) = match event {
Ok(e) => e,
Err(e) => return Poll::Ready(Some(Err(e))),
};
// handle key presses
if let CaptureEvent::Input(Event::Keyboard(KeyboardEvent::Key { key, state, .. })) = event {
self.update_pressed_keys(key, state);
}
let len = self
.position_map
.get(&pos)
.map(|ids| ids.len())
.unwrap_or(0);
match len {
0 => Poll::Pending,
1 => Poll::Ready(Some(Ok((
self.position_map.get(&pos).expect("no id")[0],
event,
)))),
_ => {
let mut position_map = HashMap::new();
swap(&mut self.position_map, &mut position_map);
{
for &id in position_map.get(&pos).expect("position") {
self.pending.push_back((id, event));
}
}
swap(&mut self.position_map, &mut position_map);
Poll::Ready(Some(Ok(self.pending.pop_front().expect("event"))))
}
}
}
}
#[async_trait]
trait Capture: Stream<Item = Result<(Position, CaptureEvent), CaptureError>> + Unpin {
/// create a new client with the given id
async fn create(&mut self, pos: Position) -> Result<(), CaptureError>;
/// destroy the client with the given id, if it exists
async fn destroy(&mut self, pos: Position) -> Result<(), CaptureError>;
/// release mouse
async fn release(&mut self) -> Result<(), CaptureError>;
/// destroy the input capture
async fn terminate(&mut self) -> Result<(), CaptureError>;
}
async fn create_backend(
backend: Backend,
) -> Result<
Box<dyn Capture<Item = Result<(Position, CaptureEvent), CaptureError>>>,
CaptureCreationError,
> {
match backend {
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
Backend::InputCapturePortal => Ok(Box::new(libei::LibeiInputCapture::new().await?)),
#[cfg(all(unix, feature = "layer_shell", not(target_os = "macos")))]
Backend::LayerShell => Ok(Box::new(layer_shell::LayerShellInputCapture::new()?)),
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
Backend::X11 => Ok(Box::new(x11::X11InputCapture::new()?)),
#[cfg(windows)]
Backend::Windows => Ok(Box::new(windows::WindowsInputCapture::new())),
#[cfg(target_os = "macos")]
Backend::MacOs => Ok(Box::new(macos::MacOSInputCapture::new().await?)),
Backend::Dummy => Ok(Box::new(dummy::DummyInputCapture::new())),
}
}
async fn create(
backend: Option<Backend>,
) -> Result<
Box<dyn Capture<Item = Result<(Position, CaptureEvent), CaptureError>>>,
CaptureCreationError,
> {
if let Some(backend) = backend {
let b = create_backend(backend).await;
if b.is_ok() {
log::info!("using capture backend: {backend}");
}
return b;
}
for backend in [
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
Backend::InputCapturePortal,
#[cfg(all(unix, feature = "layer_shell", not(target_os = "macos")))]
Backend::LayerShell,
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
Backend::X11,
#[cfg(windows)]
Backend::Windows,
#[cfg(target_os = "macos")]
Backend::MacOs,
] {
match create_backend(backend).await {
Ok(b) => {
log::info!("using capture backend: {backend}");
return Ok(b);
}
Err(e) if e.cancelled_by_user() => return Err(e),
Err(e) => log::warn!("{backend} input capture backend unavailable: {e}"),
}
}
Err(CaptureCreationError::NoAvailableBackend)
}

618
input-capture/src/libei.rs Normal file
View File

@@ -0,0 +1,618 @@
use ashpd::{
desktop::{
input_capture::{
Activated, ActivatedBarrier, Barrier, BarrierID, Capabilities, InputCapture, Region,
Zones,
},
Session,
},
enumflags2::BitFlags,
};
use async_trait::async_trait;
use futures::{FutureExt, StreamExt};
use reis::{
ei::{self, handshake::ContextType},
event::{Connection, DeviceCapability, EiEvent},
tokio::EiConvertEventStream,
};
use std::{
cell::Cell,
collections::HashMap,
io,
num::NonZeroU32,
os::unix::net::UnixStream,
pin::Pin,
rc::Rc,
sync::Arc,
task::{Context, Poll},
};
use tokio::{
sync::{
mpsc::{self, Receiver, Sender},
Notify,
},
task::JoinHandle,
};
use tokio_util::sync::CancellationToken;
use futures_core::Stream;
use input_event::Event;
use crate::CaptureEvent;
use super::{
error::{CaptureError, LibeiCaptureCreationError},
Capture as LanMouseInputCapture, Position,
};
/* there is a bug in xdg-remote-desktop-portal-gnome / mutter that
* prevents receiving further events after a session has been disabled once.
* Therefore the session needs to be recreated when the barriers are updated */
/// events that necessitate restarting the capture session
#[derive(Clone, Copy, Debug)]
enum LibeiNotifyEvent {
Create(Position),
Destroy(Position),
}
#[allow(dead_code)]
pub struct LibeiInputCapture<'a> {
input_capture: Pin<Box<InputCapture<'a>>>,
capture_task: JoinHandle<Result<(), CaptureError>>,
event_rx: Receiver<(Position, CaptureEvent)>,
notify_capture: Sender<LibeiNotifyEvent>,
notify_release: Arc<Notify>,
cancellation_token: CancellationToken,
terminated: bool,
}
/// returns (start pos, end pos), inclusive
fn pos_to_barrier(r: &Region, pos: Position) -> (i32, i32, i32, i32) {
let (x, y) = (r.x_offset(), r.y_offset());
let (w, h) = (r.width() as i32, r.height() as i32);
match pos {
Position::Left => (x, y, x, y + h - 1),
Position::Right => (x + w, y, x + w, y + h - 1),
Position::Top => (x, y, x + w - 1, y),
Position::Bottom => (x, y + h, x + w - 1, y + h),
}
}
/// Ashpd does not expose fields
#[derive(Clone, Copy, Debug)]
struct ICBarrier {
barrier_id: BarrierID,
position: (i32, i32, i32, i32),
}
impl ICBarrier {
fn new(barrier_id: BarrierID, position: (i32, i32, i32, i32)) -> Self {
Self {
barrier_id,
position,
}
}
}
impl From<ICBarrier> for Barrier {
fn from(barrier: ICBarrier) -> Self {
Barrier::new(barrier.barrier_id, barrier.position)
}
}
fn select_barriers(
zones: &Zones,
clients: &[Position],
next_barrier_id: &mut NonZeroU32,
) -> (Vec<ICBarrier>, HashMap<BarrierID, Position>) {
let mut pos_for_barrier = HashMap::new();
let mut barriers: Vec<ICBarrier> = vec![];
for pos in clients {
let mut client_barriers = zones
.regions()
.iter()
.map(|r| {
let id = *next_barrier_id;
*next_barrier_id = next_barrier_id
.checked_add(1)
.expect("barrier id out of range");
let position = pos_to_barrier(r, *pos);
pos_for_barrier.insert(id, *pos);
ICBarrier::new(id, position)
})
.collect();
barriers.append(&mut client_barriers);
}
(barriers, pos_for_barrier)
}
async fn update_barriers(
input_capture: &InputCapture<'_>,
session: &Session<'_, InputCapture<'_>>,
active_clients: &[Position],
next_barrier_id: &mut NonZeroU32,
) -> Result<(Vec<ICBarrier>, HashMap<BarrierID, Position>), ashpd::Error> {
let zones = input_capture.zones(session).await?.response()?;
log::debug!("zones: {zones:?}");
let (barriers, id_map) = select_barriers(&zones, active_clients, next_barrier_id);
log::debug!("barriers: {barriers:?}");
log::debug!("client for barrier id: {id_map:?}");
let ashpd_barriers: Vec<Barrier> = barriers.iter().copied().map(|b| b.into()).collect();
let response = input_capture
.set_pointer_barriers(session, &ashpd_barriers, zones.zone_set())
.await?;
let response = response.response()?;
log::debug!("{response:?}");
Ok((barriers, id_map))
}
async fn create_session<'a>(
input_capture: &'a InputCapture<'a>,
) -> std::result::Result<(Session<'_, InputCapture<'_>>, BitFlags<Capabilities>), ashpd::Error> {
log::debug!("creating input capture session");
input_capture
.create_session(
None,
Capabilities::Keyboard | Capabilities::Pointer | Capabilities::Touchscreen,
)
.await
}
async fn connect_to_eis(
input_capture: &InputCapture<'_>,
session: &Session<'_, InputCapture<'_>>,
) -> Result<(ei::Context, Connection, EiConvertEventStream), CaptureError> {
log::debug!("connect_to_eis");
let fd = input_capture.connect_to_eis(session).await?;
// create unix stream from fd
let stream = UnixStream::from(fd);
stream.set_nonblocking(true)?;
// create ei context
let context = ei::Context::new(stream)?;
let (conn, event_stream) = context
.handshake_tokio("de.feschber.LanMouse", ContextType::Receiver)
.await?;
Ok((context, conn, event_stream))
}
async fn libei_event_handler(
mut ei_event_stream: EiConvertEventStream,
context: ei::Context,
event_tx: Sender<(Position, CaptureEvent)>,
release_session: Arc<Notify>,
current_pos: Rc<Cell<Option<Position>>>,
) -> Result<(), CaptureError> {
loop {
let ei_event = ei_event_stream
.next()
.await
.ok_or(CaptureError::EndOfStream)??;
log::trace!("from ei: {ei_event:?}");
let client = current_pos.get();
handle_ei_event(ei_event, client, &context, &event_tx, &release_session).await?;
}
}
impl<'a> LibeiInputCapture<'a> {
pub async fn new() -> std::result::Result<Self, LibeiCaptureCreationError> {
let input_capture = Box::pin(InputCapture::new().await?);
let input_capture_ptr = input_capture.as_ref().get_ref() as *const InputCapture<'static>;
let first_session = Some(create_session(unsafe { &*input_capture_ptr }).await?);
let (event_tx, event_rx) = mpsc::channel(1);
let (notify_capture, notify_rx) = mpsc::channel(1);
let notify_release = Arc::new(Notify::new());
let cancellation_token = CancellationToken::new();
let capture = do_capture(
input_capture_ptr,
notify_rx,
notify_release.clone(),
first_session,
event_tx,
cancellation_token.clone(),
);
let capture_task = tokio::task::spawn_local(capture);
let producer = Self {
input_capture,
event_rx,
capture_task,
notify_capture,
notify_release,
cancellation_token,
terminated: false,
};
Ok(producer)
}
}
async fn do_capture(
input_capture: *const InputCapture<'static>,
mut capture_event: Receiver<LibeiNotifyEvent>,
notify_release: Arc<Notify>,
session: Option<(Session<'_, InputCapture<'_>>, BitFlags<Capabilities>)>,
event_tx: Sender<(Position, CaptureEvent)>,
cancellation_token: CancellationToken,
) -> Result<(), CaptureError> {
let mut session = session.map(|s| s.0);
/* safety: libei_task does not outlive Self */
let input_capture = unsafe { &*input_capture };
let mut active_clients: Vec<Position> = vec![];
let mut next_barrier_id = NonZeroU32::new(1).expect("id must be non-zero");
let mut zones_changed = input_capture.receive_zones_changed().await?;
loop {
// do capture session
let cancel_session = CancellationToken::new();
let cancel_update = CancellationToken::new();
let mut capture_event_occured: Option<LibeiNotifyEvent> = None;
let mut zones_have_changed = false;
// kill session if clients need to be updated
let handle_session_update_request = async {
tokio::select! {
_ = cancellation_token.cancelled() => {
log::debug!("cancelled")
}, /* exit requested */
_ = cancel_update.cancelled() => {
log::debug!("update task cancelled");
}, /* session exited */
_ = zones_changed.next() => {
log::debug!("zones changed!");
zones_have_changed = true
}, /* zones have changed */
e = capture_event.recv() => if let Some(e) = e { /* clients changed */
log::debug!("capture event: {e:?}");
capture_event_occured.replace(e);
},
}
// kill session (might already be dead!)
log::debug!("=> cancelling session");
cancel_session.cancel();
};
if !active_clients.is_empty() {
// create session
let mut session = match session.take() {
Some(s) => s,
None => create_session(input_capture).await?.0,
};
let capture_session = do_capture_session(
input_capture,
&mut session,
&event_tx,
&active_clients,
&mut next_barrier_id,
&notify_release,
(cancel_session.clone(), cancel_update.clone()),
);
let (capture_result, ()) = tokio::join!(capture_session, handle_session_update_request);
log::debug!("capture session + session_update task done!");
// disable capture
log::debug!("disabling input capture");
if let Err(e) = input_capture.disable(&session).await {
log::warn!("input_capture.disable(&session) {e}");
}
if let Err(e) = session.close().await {
log::warn!("session.close(): {e}");
}
// propagate error from capture session
capture_result?;
} else {
handle_session_update_request.await;
}
// update clients if requested
if let Some(event) = capture_event_occured.take() {
match event {
LibeiNotifyEvent::Create(p) => active_clients.push(p),
LibeiNotifyEvent::Destroy(p) => active_clients.retain(|&pos| pos != p),
}
}
// break
if cancellation_token.is_cancelled() {
break Ok(());
}
}
}
async fn do_capture_session(
input_capture: &InputCapture<'_>,
session: &mut Session<'_, InputCapture<'_>>,
event_tx: &Sender<(Position, CaptureEvent)>,
active_clients: &[Position],
next_barrier_id: &mut NonZeroU32,
notify_release: &Notify,
cancel: (CancellationToken, CancellationToken),
) -> Result<(), CaptureError> {
let (cancel_session, cancel_update) = cancel;
// current client
let current_pos = Rc::new(Cell::new(None));
// connect to eis server
let (context, _conn, ei_event_stream) = connect_to_eis(input_capture, session).await?;
// set barriers
let (barriers, pos_for_barrier_id) =
update_barriers(input_capture, session, active_clients, next_barrier_id).await?;
log::debug!("enabling session");
input_capture.enable(session).await?;
// cancellation token to release session
let release_session = Arc::new(Notify::new());
// async event task
let cancel_ei_handler = CancellationToken::new();
let event_chan = event_tx.clone();
let pos = current_pos.clone();
let cancel_session_clone = cancel_session.clone();
let release_session_clone = release_session.clone();
let cancel_ei_handler_clone = cancel_ei_handler.clone();
let ei_task = async move {
tokio::select! {
r = libei_event_handler(
ei_event_stream,
context,
event_chan,
release_session_clone,
pos,
) => {
log::debug!("libei exited: {r:?} cancelling session task");
cancel_session_clone.cancel();
}
_ = cancel_ei_handler_clone.cancelled() => {},
}
Ok::<(), CaptureError>(())
};
let capture_session_task = async {
// receiver for activation tokens
let mut activated = input_capture.receive_activated().await?;
let mut ei_devices_changed = false;
loop {
tokio::select! {
activated = activated.next() => {
let activated = activated.ok_or(CaptureError::ActivationClosed)?;
log::debug!("activated: {activated:?}");
// get barrier id from activation
let barrier_id = match activated.barrier_id() {
Some(ActivatedBarrier::Barrier(id)) => id,
// workaround for KDE plasma not reporting barrier ids
Some(ActivatedBarrier::UnknownBarrier) | None => find_corresponding_client(&barriers, activated.cursor_position().expect("no cursor position reported by compositor")),
};
// find client corresponding to barrier
let pos = *pos_for_barrier_id.get(&barrier_id).expect("invalid barrier id");
current_pos.replace(Some(pos));
// client entered => send event
event_tx.send((pos, CaptureEvent::Begin)).await.expect("no channel");
tokio::select! {
_ = notify_release.notified() => { /* capture release */
log::debug!("release session requested");
},
_ = release_session.notified() => { /* release session */
log::debug!("ei devices changed");
ei_devices_changed = true;
},
_ = cancel_session.cancelled() => { /* kill session notify */
log::debug!("session cancel requested");
break
},
}
release_capture(input_capture, session, activated, pos).await?;
}
_ = notify_release.notified() => { /* capture release -> we are not capturing anyway, so ignore */
log::debug!("release session requested");
},
_ = release_session.notified() => { /* release session */
log::debug!("ei devices changed");
ei_devices_changed = true;
},
_ = cancel_session.cancelled() => { /* kill session notify */
log::debug!("session cancel requested");
break
},
}
if ei_devices_changed {
/* for whatever reason, GNOME seems to kill the session
* as soon as devices are added or removed, so we need
* to cancel */
break;
}
}
// cancel libei task
log::debug!("session exited: killing libei task");
cancel_ei_handler.cancel();
Ok::<(), CaptureError>(())
};
let (a, b) = tokio::join!(ei_task, capture_session_task);
cancel_update.cancel();
log::debug!("both session and ei task finished!");
a?;
b?;
Ok(())
}
async fn release_capture<'a>(
input_capture: &InputCapture<'a>,
session: &Session<'a, InputCapture<'a>>,
activated: Activated,
current_pos: Position,
) -> Result<(), CaptureError> {
if let Some(activation_id) = activated.activation_id() {
log::debug!("releasing input capture {activation_id}");
}
let (x, y) = activated
.cursor_position()
.expect("compositor did not report cursor position!");
log::debug!("client entered @ ({x}, {y})");
let (dx, dy) = match current_pos {
// offset cursor position to not enter again immediately
Position::Left => (1., 0.),
Position::Right => (-1., 0.),
Position::Top => (0., 1.),
Position::Bottom => (0., -1.),
};
// release 1px to the right of the entered zone
let cursor_position = (x as f64 + dx, y as f64 + dy);
input_capture
.release(session, activated.activation_id(), Some(cursor_position))
.await?;
Ok(())
}
fn find_corresponding_client(barriers: &[ICBarrier], pos: (f32, f32)) -> BarrierID {
barriers
.iter()
.copied()
.min_by_key(|b| {
let (x1, y1, x2, y2) = b.position;
let (x1, y1, x2, y2) = (x1 as f32, y1 as f32, x2 as f32, y2 as f32);
distance_to_line(((x1, y1), (x2, y2)), pos) as i32
})
.expect("could not find barrier corresponding to client")
.barrier_id
}
fn distance_to_line(line: ((f32, f32), (f32, f32)), p: (f32, f32)) -> f32 {
let ((x1, y1), (x2, y2)) = line;
let (x0, y0) = p;
/*
* we use the fact that for the triangle spanned by the line and p,
* the height of the triangle is the desired distance and can be calculated by
* h = 2A / b with b being the line_length and
*/
let double_triangle_area = ((y2 - y1) * x0 - (x2 - x1) * y0 + x2 * y1 - y2 * x1).abs();
let line_length = ((y2 - y1).powf(2.0) + (x2 - x1).powf(2.0)).sqrt();
let distance = double_triangle_area / line_length;
log::debug!("distance to line({line:?}, {p:?}) = {distance}");
distance
}
static ALL_CAPABILITIES: &[DeviceCapability] = &[
DeviceCapability::Pointer,
DeviceCapability::PointerAbsolute,
DeviceCapability::Keyboard,
DeviceCapability::Touch,
DeviceCapability::Scroll,
DeviceCapability::Button,
];
async fn handle_ei_event(
ei_event: EiEvent,
current_client: Option<Position>,
context: &ei::Context,
event_tx: &Sender<(Position, CaptureEvent)>,
release_session: &Notify,
) -> Result<(), CaptureError> {
match ei_event {
EiEvent::SeatAdded(s) => {
s.seat.bind_capabilities(ALL_CAPABILITIES);
context.flush().map_err(|e| io::Error::new(e.kind(), e))?;
}
EiEvent::SeatRemoved(_) | /* EiEvent::DeviceAdded(_) | */ EiEvent::DeviceRemoved(_) => {
log::debug!("releasing session: {ei_event:?}");
release_session.notify_waiters();
}
EiEvent::DevicePaused(_) | EiEvent::DeviceResumed(_) => {}
EiEvent::DeviceStartEmulating(_) => log::debug!("START EMULATING"),
EiEvent::DeviceStopEmulating(_) => log::debug!("STOP EMULATING"),
EiEvent::Disconnected(d) => {
return Err(CaptureError::Disconnected(format!("{:?}", d.reason)))
}
_ => {
if let Some(pos) = current_client {
for event in Event::from_ei_event(ei_event) {
event_tx.send((pos, CaptureEvent::Input(event))).await.expect("no channel");
}
}
}
}
Ok(())
}
#[async_trait]
impl<'a> LanMouseInputCapture for LibeiInputCapture<'a> {
async fn create(&mut self, pos: Position) -> Result<(), CaptureError> {
let _ = self
.notify_capture
.send(LibeiNotifyEvent::Create(pos))
.await;
Ok(())
}
async fn destroy(&mut self, pos: Position) -> Result<(), CaptureError> {
let _ = self
.notify_capture
.send(LibeiNotifyEvent::Destroy(pos))
.await;
Ok(())
}
async fn release(&mut self) -> Result<(), CaptureError> {
self.notify_release.notify_waiters();
Ok(())
}
async fn terminate(&mut self) -> Result<(), CaptureError> {
self.cancellation_token.cancel();
let task = &mut self.capture_task;
log::debug!("waiting for capture to terminate...");
let res = task.await.expect("libei task panic");
log::debug!("done!");
self.terminated = true;
res
}
}
impl<'a> Drop for LibeiInputCapture<'a> {
fn drop(&mut self) {
if !self.terminated {
/* this workaround is needed until async drop is stabilized */
panic!("LibeiInputCapture dropped without being terminated!");
}
}
}
impl<'a> Stream for LibeiInputCapture<'a> {
type Item = Result<(Position, CaptureEvent), CaptureError>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
match self.capture_task.poll_unpin(cx) {
Poll::Ready(r) => match r.expect("failed to join") {
Ok(()) => Poll::Ready(None),
Err(e) => Poll::Ready(Some(Err(e))),
},
Poll::Pending => self.event_rx.poll_recv(cx).map(|e| e.map(Result::Ok)),
}
}
}

618
input-capture/src/macos.rs Normal file
View File

@@ -0,0 +1,618 @@
use super::{error::MacosCaptureCreationError, Capture, CaptureError, CaptureEvent, Position};
use async_trait::async_trait;
use bitflags::bitflags;
use core_foundation::base::{kCFAllocatorDefault, CFRelease};
use core_foundation::date::CFTimeInterval;
use core_foundation::number::{kCFBooleanTrue, CFBooleanRef};
use core_foundation::runloop::{kCFRunLoopCommonModes, CFRunLoop, CFRunLoopSource};
use core_foundation::string::{kCFStringEncodingUTF8, CFStringCreateWithCString, CFStringRef};
use core_graphics::base::{kCGErrorSuccess, CGError};
use core_graphics::display::{CGDisplay, CGPoint};
use core_graphics::event::{
CGEvent, CGEventFlags, CGEventTap, CGEventTapLocation, CGEventTapOptions, CGEventTapPlacement,
CGEventTapProxy, CGEventType, EventField,
};
use core_graphics::event_source::{CGEventSource, CGEventSourceStateID};
use futures_core::Stream;
use input_event::{Event, KeyboardEvent, PointerEvent, BTN_LEFT, BTN_MIDDLE, BTN_RIGHT};
use keycode::{KeyMap, KeyMapping};
use libc::c_void;
use once_cell::unsync::Lazy;
use std::collections::HashSet;
use std::ffi::{c_char, CString};
use std::pin::Pin;
use std::sync::Arc;
use std::task::{ready, Context, Poll};
use std::thread::{self};
use tokio::sync::mpsc::{self, Receiver, Sender};
use tokio::sync::{oneshot, Mutex};
#[derive(Debug, Default)]
struct Bounds {
xmin: f64,
xmax: f64,
ymin: f64,
ymax: f64,
}
#[derive(Debug)]
struct InputCaptureState {
active_clients: Lazy<HashSet<Position>>,
current_pos: Option<Position>,
bounds: Bounds,
}
#[derive(Debug)]
enum ProducerEvent {
Release,
Create(Position),
Destroy(Position),
Grab(Position),
EventTapDisabled,
}
impl InputCaptureState {
fn new() -> Result<Self, MacosCaptureCreationError> {
let mut res = Self {
active_clients: Lazy::new(HashSet::new),
current_pos: None,
bounds: Bounds::default(),
};
res.update_bounds()?;
Ok(res)
}
fn crossed(&mut self, event: &CGEvent) -> Option<Position> {
let location = event.location();
let relative_x = event.get_double_value_field(EventField::MOUSE_EVENT_DELTA_X);
let relative_y = event.get_double_value_field(EventField::MOUSE_EVENT_DELTA_Y);
for &position in self.active_clients.iter() {
if (position == Position::Left && (location.x + relative_x) <= self.bounds.xmin)
|| (position == Position::Right && (location.x + relative_x) >= self.bounds.xmax)
|| (position == Position::Top && (location.y + relative_y) <= self.bounds.ymin)
|| (position == Position::Bottom && (location.y + relative_y) >= self.bounds.ymax)
{
log::debug!("Crossed barrier into position: {position:?}");
return Some(position);
}
}
None
}
// Get the max bounds of all displays
fn update_bounds(&mut self) -> Result<(), MacosCaptureCreationError> {
let active_ids =
CGDisplay::active_displays().map_err(MacosCaptureCreationError::ActiveDisplays)?;
active_ids.iter().for_each(|d| {
let bounds = CGDisplay::new(*d).bounds();
self.bounds.xmin = self.bounds.xmin.min(bounds.origin.x);
self.bounds.xmax = self.bounds.xmax.max(bounds.origin.x + bounds.size.width);
self.bounds.ymin = self.bounds.ymin.min(bounds.origin.y);
self.bounds.ymax = self.bounds.ymax.max(bounds.origin.y + bounds.size.height);
});
log::debug!("Updated displays bounds: {0:?}", self.bounds);
Ok(())
}
// We can't disable mouse movement when in a client so we need to reset the cursor position
// to the edge of the screen, the cursor will be hidden but we dont want it to appear in a
// random location when we exit the client
fn reset_mouse_position(&self, event: &CGEvent) -> Result<(), CaptureError> {
if let Some(pos) = self.current_pos {
let location = event.location();
let edge_offset = 1.0;
// After the cursor is warped no event is produced but the next event
// will carry the delta from the warp so only half the delta is needed to move the cursor
let delta_y = event.get_double_value_field(EventField::MOUSE_EVENT_DELTA_Y) / 2.0;
let delta_x = event.get_double_value_field(EventField::MOUSE_EVENT_DELTA_X) / 2.0;
let mut new_x = location.x + delta_x;
let mut new_y = location.y + delta_y;
match pos {
Position::Left => {
new_x = self.bounds.xmin + edge_offset;
}
Position::Right => {
new_x = self.bounds.xmax - edge_offset;
}
Position::Top => {
new_y = self.bounds.ymin + edge_offset;
}
Position::Bottom => {
new_y = self.bounds.ymax - edge_offset;
}
}
let new_pos = CGPoint::new(new_x, new_y);
log::trace!("Resetting cursor position to: {new_x}, {new_y}");
return CGDisplay::warp_mouse_cursor_position(new_pos)
.map_err(CaptureError::WarpCursor);
}
Err(CaptureError::ResetMouseWithoutClient)
}
async fn handle_producer_event(
&mut self,
producer_event: ProducerEvent,
) -> Result<(), CaptureError> {
log::debug!("handling event: {producer_event:?}");
match producer_event {
ProducerEvent::Release => {
if self.current_pos.is_some() {
CGDisplay::show_cursor(&CGDisplay::main())
.map_err(CaptureError::CoreGraphics)?;
self.current_pos = None;
}
}
ProducerEvent::Grab(pos) => {
if self.current_pos.is_none() {
CGDisplay::hide_cursor(&CGDisplay::main())
.map_err(CaptureError::CoreGraphics)?;
self.current_pos = Some(pos);
}
}
ProducerEvent::Create(p) => {
self.active_clients.insert(p);
}
ProducerEvent::Destroy(p) => {
if let Some(current) = self.current_pos {
if current == p {
CGDisplay::show_cursor(&CGDisplay::main())
.map_err(CaptureError::CoreGraphics)?;
self.current_pos = None;
};
}
self.active_clients.remove(&p);
}
ProducerEvent::EventTapDisabled => return Err(CaptureError::EventTapDisabled),
};
Ok(())
}
}
fn get_events(
ev_type: &CGEventType,
ev: &CGEvent,
result: &mut Vec<CaptureEvent>,
) -> Result<(), CaptureError> {
fn map_pointer_event(ev: &CGEvent) -> PointerEvent {
PointerEvent::Motion {
time: 0,
dx: ev.get_double_value_field(EventField::MOUSE_EVENT_DELTA_X),
dy: ev.get_double_value_field(EventField::MOUSE_EVENT_DELTA_Y),
}
}
fn map_key(ev: &CGEvent) -> Result<u32, CaptureError> {
let code = ev.get_integer_value_field(EventField::KEYBOARD_EVENT_KEYCODE);
match KeyMap::from_key_mapping(KeyMapping::Mac(code as u16)) {
Ok(k) => Ok(k.evdev as u32),
Err(()) => Err(CaptureError::KeyMapError(code)),
}
}
match ev_type {
CGEventType::KeyDown => {
let k = map_key(ev)?;
result.push(CaptureEvent::Input(Event::Keyboard(KeyboardEvent::Key {
time: 0,
key: k,
state: 1,
})));
}
CGEventType::KeyUp => {
let k = map_key(ev)?;
result.push(CaptureEvent::Input(Event::Keyboard(KeyboardEvent::Key {
time: 0,
key: k,
state: 0,
})));
}
CGEventType::FlagsChanged => {
let mut mods = XMods::empty();
let mut mods_locked = XMods::empty();
let cg_flags = ev.get_flags();
if cg_flags.contains(CGEventFlags::CGEventFlagShift) {
mods |= XMods::ShiftMask;
}
if cg_flags.contains(CGEventFlags::CGEventFlagControl) {
mods |= XMods::ControlMask;
}
if cg_flags.contains(CGEventFlags::CGEventFlagAlternate) {
mods |= XMods::Mod1Mask;
}
if cg_flags.contains(CGEventFlags::CGEventFlagCommand) {
mods |= XMods::Mod4Mask;
}
if cg_flags.contains(CGEventFlags::CGEventFlagAlphaShift) {
mods |= XMods::LockMask;
mods_locked |= XMods::LockMask;
}
let modifier_event = KeyboardEvent::Modifiers {
depressed: mods.bits(),
latched: 0,
locked: mods_locked.bits(),
group: 0,
};
result.push(CaptureEvent::Input(Event::Keyboard(modifier_event)));
}
CGEventType::MouseMoved => {
result.push(CaptureEvent::Input(Event::Pointer(map_pointer_event(ev))))
}
CGEventType::LeftMouseDragged => {
result.push(CaptureEvent::Input(Event::Pointer(map_pointer_event(ev))))
}
CGEventType::RightMouseDragged => {
result.push(CaptureEvent::Input(Event::Pointer(map_pointer_event(ev))))
}
CGEventType::OtherMouseDragged => {
result.push(CaptureEvent::Input(Event::Pointer(map_pointer_event(ev))))
}
CGEventType::LeftMouseDown => {
result.push(CaptureEvent::Input(Event::Pointer(PointerEvent::Button {
time: 0,
button: BTN_LEFT,
state: 1,
})))
}
CGEventType::LeftMouseUp => {
result.push(CaptureEvent::Input(Event::Pointer(PointerEvent::Button {
time: 0,
button: BTN_LEFT,
state: 0,
})))
}
CGEventType::RightMouseDown => {
result.push(CaptureEvent::Input(Event::Pointer(PointerEvent::Button {
time: 0,
button: BTN_RIGHT,
state: 1,
})))
}
CGEventType::RightMouseUp => {
result.push(CaptureEvent::Input(Event::Pointer(PointerEvent::Button {
time: 0,
button: BTN_RIGHT,
state: 0,
})))
}
CGEventType::OtherMouseDown => {
result.push(CaptureEvent::Input(Event::Pointer(PointerEvent::Button {
time: 0,
button: BTN_MIDDLE,
state: 1,
})))
}
CGEventType::OtherMouseUp => {
result.push(CaptureEvent::Input(Event::Pointer(PointerEvent::Button {
time: 0,
button: BTN_MIDDLE,
state: 0,
})))
}
CGEventType::ScrollWheel => {
let v = ev.get_integer_value_field(EventField::SCROLL_WHEEL_EVENT_POINT_DELTA_AXIS_1);
let h = ev.get_integer_value_field(EventField::SCROLL_WHEEL_EVENT_POINT_DELTA_AXIS_2);
if v != 0 {
result.push(CaptureEvent::Input(Event::Pointer(PointerEvent::Axis {
time: 0,
axis: 0, // Vertical
value: v as f64,
})));
}
if h != 0 {
result.push(CaptureEvent::Input(Event::Pointer(PointerEvent::Axis {
time: 0,
axis: 1, // Horizontal
value: h as f64,
})));
}
}
_ => (),
}
Ok(())
}
fn create_event_tap<'a>(
client_state: Arc<Mutex<InputCaptureState>>,
notify_tx: Sender<ProducerEvent>,
event_tx: Sender<(Position, CaptureEvent)>,
) -> Result<CGEventTap<'a>, MacosCaptureCreationError> {
let cg_events_of_interest: Vec<CGEventType> = vec![
CGEventType::LeftMouseDown,
CGEventType::LeftMouseUp,
CGEventType::RightMouseDown,
CGEventType::RightMouseUp,
CGEventType::OtherMouseDown,
CGEventType::OtherMouseUp,
CGEventType::MouseMoved,
CGEventType::LeftMouseDragged,
CGEventType::RightMouseDragged,
CGEventType::OtherMouseDragged,
CGEventType::ScrollWheel,
CGEventType::KeyDown,
CGEventType::KeyUp,
CGEventType::FlagsChanged,
];
let event_tap_callback =
move |_proxy: CGEventTapProxy, event_type: CGEventType, cg_ev: &CGEvent| {
log::trace!("Got event from tap: {event_type:?}");
let mut state = client_state.blocking_lock();
let mut pos = None;
let mut res_events = vec![];
if matches!(
event_type,
CGEventType::TapDisabledByTimeout | CGEventType::TapDisabledByUserInput
) {
log::error!("CGEventTap disabled");
notify_tx
.blocking_send(ProducerEvent::EventTapDisabled)
.unwrap_or_else(|e| {
log::error!("Failed to send notification: {e}");
});
}
// Are we in a client?
if let Some(current_pos) = state.current_pos {
pos = Some(current_pos);
get_events(&event_type, cg_ev, &mut res_events).unwrap_or_else(|e| {
log::error!("Failed to get events: {e}");
});
// Keep (hidden) cursor at the edge of the screen
if matches!(event_type, CGEventType::MouseMoved) {
state.reset_mouse_position(cg_ev).unwrap_or_else(|e| {
log::error!("Failed to reset mouse position: {e}");
})
}
}
// Did we cross a barrier?
else if matches!(event_type, CGEventType::MouseMoved) {
if let Some(new_pos) = state.crossed(cg_ev) {
pos = Some(new_pos);
res_events.push(CaptureEvent::Begin);
notify_tx
.blocking_send(ProducerEvent::Grab(new_pos))
.expect("Failed to send notification");
}
}
if let Some(pos) = pos {
res_events.iter().for_each(|e| {
event_tx
.blocking_send((pos, *e))
.expect("Failed to send event");
});
// Returning None should stop the event from being processed
// but core fundation still returns the event
cg_ev.set_type(CGEventType::Null);
}
Some(cg_ev.to_owned())
};
let tap = CGEventTap::new(
CGEventTapLocation::Session,
CGEventTapPlacement::HeadInsertEventTap,
CGEventTapOptions::Default,
cg_events_of_interest,
event_tap_callback,
)
.map_err(|_| MacosCaptureCreationError::EventTapCreation)?;
let tap_source: CFRunLoopSource = tap
.mach_port
.create_runloop_source(0)
.expect("Failed creating loop source");
unsafe {
CFRunLoop::get_current().add_source(&tap_source, kCFRunLoopCommonModes);
}
Ok(tap)
}
fn event_tap_thread(
client_state: Arc<Mutex<InputCaptureState>>,
event_tx: Sender<(Position, CaptureEvent)>,
notify_tx: Sender<ProducerEvent>,
ready: std::sync::mpsc::Sender<Result<(), MacosCaptureCreationError>>,
exit: oneshot::Sender<Result<(), &'static str>>,
) {
let _tap = match create_event_tap(client_state, notify_tx, event_tx) {
Err(e) => {
ready.send(Err(e)).expect("channel closed");
return;
}
Ok(tap) => {
ready.send(Ok(())).expect("channel closed");
tap
}
};
CFRunLoop::run_current();
let _ = exit.send(Err("tap thread exited"));
}
pub struct MacOSInputCapture {
event_rx: Receiver<(Position, CaptureEvent)>,
notify_tx: Sender<ProducerEvent>,
}
impl MacOSInputCapture {
pub async fn new() -> Result<Self, MacosCaptureCreationError> {
let state = Arc::new(Mutex::new(InputCaptureState::new()?));
let (event_tx, event_rx) = mpsc::channel(32);
let (notify_tx, mut notify_rx) = mpsc::channel(32);
let (ready_tx, ready_rx) = std::sync::mpsc::channel();
let (tap_exit_tx, mut tap_exit_rx) = oneshot::channel();
unsafe {
configure_cf_settings()?;
}
log::info!("Enabling CGEvent tap");
let event_tap_thread_state = state.clone();
let event_tap_notify = notify_tx.clone();
thread::spawn(move || {
event_tap_thread(
event_tap_thread_state,
event_tx,
event_tap_notify,
ready_tx,
tap_exit_tx,
)
});
// wait for event tap creation result
ready_rx.recv().expect("channel closed")?;
let _tap_task: tokio::task::JoinHandle<()> = tokio::task::spawn_local(async move {
loop {
tokio::select! {
producer_event = notify_rx.recv() => {
let producer_event = producer_event.expect("channel closed");
let mut state = state.lock().await;
state.handle_producer_event(producer_event).await.unwrap_or_else(|e| {
log::error!("Failed to handle producer event: {e}");
})
}
res = &mut tap_exit_rx => {
if let Err(e) = res.expect("channel closed") {
log::error!("Tap thread failed: {:?}", e);
break;
}
}
}
}
});
Ok(Self {
event_rx,
notify_tx,
})
}
}
#[async_trait]
impl Capture for MacOSInputCapture {
async fn create(&mut self, pos: Position) -> Result<(), CaptureError> {
let notify_tx = self.notify_tx.clone();
tokio::task::spawn_local(async move {
log::debug!("creating capture, {pos}");
let _ = notify_tx.send(ProducerEvent::Create(pos)).await;
log::debug!("done !");
});
Ok(())
}
async fn destroy(&mut self, pos: Position) -> Result<(), CaptureError> {
let notify_tx = self.notify_tx.clone();
tokio::task::spawn_local(async move {
log::debug!("destroying capture {pos}");
let _ = notify_tx.send(ProducerEvent::Destroy(pos)).await;
log::debug!("done !");
});
Ok(())
}
async fn release(&mut self) -> Result<(), CaptureError> {
let notify_tx = self.notify_tx.clone();
tokio::task::spawn_local(async move {
log::debug!("notifying Release");
let _ = notify_tx.send(ProducerEvent::Release).await;
});
Ok(())
}
async fn terminate(&mut self) -> Result<(), CaptureError> {
Ok(())
}
}
impl Stream for MacOSInputCapture {
type Item = Result<(Position, CaptureEvent), CaptureError>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
match ready!(self.event_rx.poll_recv(cx)) {
None => Poll::Ready(None),
Some(e) => Poll::Ready(Some(Ok(e))),
}
}
}
type CGSConnectionID = u32;
#[link(name = "ApplicationServices", kind = "framework")]
extern "C" {
fn CGSSetConnectionProperty(
cid: CGSConnectionID,
targetCID: CGSConnectionID,
key: CFStringRef,
value: CFBooleanRef,
) -> CGError;
fn _CGSDefaultConnection() -> CGSConnectionID;
}
extern "C" {
fn CGEventSourceSetLocalEventsSuppressionInterval(
event_source: CGEventSource,
seconds: CFTimeInterval,
);
}
unsafe fn configure_cf_settings() -> Result<(), MacosCaptureCreationError> {
// When we warp the cursor using CGWarpMouseCursorPosition local events are suppressed for a short time
// this leeds to the cursor not flowing when crossing back from a clinet, set this to to 0 stops the warp
// from working, set a low value by trial and error, 0.05s seems good. 0.25s is the default
let event_source = CGEventSource::new(CGEventSourceStateID::CombinedSessionState)
.map_err(|_| MacosCaptureCreationError::EventSourceCreation)?;
CGEventSourceSetLocalEventsSuppressionInterval(event_source, 0.05);
// This is a private settings that allows the cursor to be hidden while in the background.
// It is used by Barrier and other apps.
let key = CString::new("SetsCursorInBackground").unwrap();
let cf_key = CFStringCreateWithCString(
kCFAllocatorDefault,
key.as_ptr() as *const c_char,
kCFStringEncodingUTF8,
);
if CGSSetConnectionProperty(
_CGSDefaultConnection(),
_CGSDefaultConnection(),
cf_key,
kCFBooleanTrue,
) != kCGErrorSuccess
{
return Err(MacosCaptureCreationError::CGCursorProperty);
}
CFRelease(cf_key as *const c_void);
Ok(())
}
// From X11/X.h
bitflags! {
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
struct XMods: u32 {
const ShiftMask = (1<<0);
const LockMask = (1<<1);
const ControlMask = (1<<2);
const Mod1Mask = (1<<3);
const Mod2Mask = (1<<4);
const Mod3Mask = (1<<5);
const Mod4Mask = (1<<6);
const Mod5Mask = (1<<7);
}
}

View File

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

44
input-capture/src/x11.rs Normal file
View File

@@ -0,0 +1,44 @@
use std::task::Poll;
use async_trait::async_trait;
use futures_core::Stream;
use super::{error::X11InputCaptureCreationError, Capture, CaptureError, CaptureEvent, Position};
pub struct X11InputCapture {}
impl X11InputCapture {
pub fn new() -> std::result::Result<Self, X11InputCaptureCreationError> {
Err(X11InputCaptureCreationError::NotImplemented)
}
}
#[async_trait]
impl Capture for X11InputCapture {
async fn create(&mut self, _pos: Position) -> Result<(), CaptureError> {
Ok(())
}
async fn destroy(&mut self, _pos: Position) -> Result<(), CaptureError> {
Ok(())
}
async fn release(&mut self) -> Result<(), CaptureError> {
Ok(())
}
async fn terminate(&mut self) -> Result<(), CaptureError> {
Ok(())
}
}
impl Stream for X11InputCapture {
type Item = Result<(Position, CaptureEvent), CaptureError>;
fn poll_next(
self: std::pin::Pin<&mut Self>,
_cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Option<Self::Item>> {
Poll::Pending
}
}

View File

@@ -0,0 +1,72 @@
[package]
name = "input-emulation"
description = "cross-platform input emulation library used by lan-mouse"
version = "0.3.0"
edition = "2021"
license = "GPL-3.0-or-later"
repository = "https://github.com/feschber/lan-mouse"
[dependencies]
async-trait = "0.1.80"
futures = "0.3.28"
log = "0.4.22"
input-event = { path = "../input-event", version = "0.3.0" }
thiserror = "2.0.0"
tokio = { version = "1.32.0", features = [
"io-util",
"io-std",
"macros",
"net",
"process",
"rt",
"sync",
"signal",
] }
once_cell = "1.19.0"
[target.'cfg(all(unix, not(target_os="macos")))'.dependencies]
wayland-client = { version = "0.31.1", optional = true }
wayland-protocols = { version = "0.32.1", features = [
"client",
"staging",
"unstable",
], optional = true }
wayland-protocols-wlr = { version = "0.3.1", features = [
"client",
], optional = true }
wayland-protocols-misc = { version = "0.3.1", features = [
"client",
], optional = true }
x11 = { version = "2.21.0", features = ["xlib", "xtest"], optional = true }
ashpd = { version = "0.10", default-features = false, features = [
"tokio",
], optional = true }
reis = { version = "0.4", features = ["tokio"], optional = true }
[target.'cfg(target_os="macos")'.dependencies]
bitflags = "2.6.0"
core-graphics = { version = "0.24.0", features = ["highsierra"] }
keycode = "0.4.0"
[target.'cfg(windows)'.dependencies]
windows = { version = "0.58.0", features = [
"Win32_System_LibraryLoader",
"Win32_System_Threading",
"Win32_Foundation",
"Win32_Graphics",
"Win32_Graphics_Gdi",
"Win32_UI_Input_KeyboardAndMouse",
"Win32_UI_WindowsAndMessaging",
] }
[features]
default = ["wlroots", "x11", "remote_desktop_portal", "libei"]
wlroots = [
"dep:wayland-client",
"dep:wayland-protocols",
"dep:wayland-protocols-wlr",
"dep:wayland-protocols-misc",
]
x11 = ["dep:x11"]
remote_desktop_portal = ["dep:ashpd"]
libei = ["dep:reis", "dep:ashpd"]

View File

@@ -0,0 +1,32 @@
use async_trait::async_trait;
use input_event::Event;
use crate::error::EmulationError;
use super::{Emulation, EmulationHandle};
#[derive(Default)]
pub(crate) struct DummyEmulation;
impl DummyEmulation {
pub(crate) fn new() -> Self {
Self {}
}
}
#[async_trait]
impl Emulation for DummyEmulation {
async fn consume(
&mut self,
event: Event,
client_handle: EmulationHandle,
) -> Result<(), EmulationError> {
log::info!("received event: ({client_handle}) {event}");
Ok(())
}
async fn create(&mut self, _: EmulationHandle) {}
async fn destroy(&mut self, _: EmulationHandle) {}
async fn terminate(&mut self) {
/* nothing to do */
}
}

View File

@@ -0,0 +1,161 @@
#[derive(Debug, Error)]
pub enum InputEmulationError {
#[error("error creating input-emulation: `{0}`")]
Create(#[from] EmulationCreationError),
#[error("error emulating input: `{0}`")]
Emulate(#[from] EmulationError),
}
#[cfg(all(
unix,
any(feature = "remote_desktop_portal", feature = "libei"),
not(target_os = "macos")
))]
use ashpd::{desktop::ResponseError, Error::Response};
use std::io;
use thiserror::Error;
#[cfg(all(unix, feature = "wlroots", not(target_os = "macos")))]
use wayland_client::{
backend::WaylandError,
globals::{BindError, GlobalError},
ConnectError, DispatchError,
};
#[derive(Debug, Error)]
pub enum EmulationError {
#[error("event stream closed")]
EndOfStream,
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
#[error("libei error: `{0}`")]
Libei(#[from] reis::Error),
#[cfg(all(unix, feature = "wlroots", not(target_os = "macos")))]
#[error("wayland error: `{0}`")]
Wayland(#[from] wayland_client::backend::WaylandError),
#[cfg(all(
unix,
any(feature = "remote_desktop_portal", feature = "libei"),
not(target_os = "macos")
))]
#[error("xdg-desktop-portal: `{0}`")]
Ashpd(#[from] ashpd::Error),
#[error("io error: `{0}`")]
Io(#[from] io::Error),
}
#[derive(Debug, Error)]
pub enum EmulationCreationError {
#[cfg(all(unix, feature = "wlroots", not(target_os = "macos")))]
#[error("wlroots backend: `{0}`")]
Wlroots(#[from] WlrootsEmulationCreationError),
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
#[error("libei backend: `{0}`")]
Libei(#[from] LibeiEmulationCreationError),
#[cfg(all(unix, feature = "remote_desktop_portal", not(target_os = "macos")))]
#[error("xdg-desktop-portal: `{0}`")]
Xdp(#[from] XdpEmulationCreationError),
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
#[error("x11: `{0}`")]
X11(#[from] X11EmulationCreationError),
#[cfg(target_os = "macos")]
#[error("macos: `{0}`")]
MacOs(#[from] MacOSEmulationCreationError),
#[cfg(windows)]
#[error("windows: `{0}`")]
Windows(#[from] WindowsEmulationCreationError),
#[error("capture error")]
NoAvailableBackend,
}
impl EmulationCreationError {
/// request was intentionally denied by the user
pub(crate) fn cancelled_by_user(&self) -> bool {
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
if matches!(
self,
EmulationCreationError::Libei(LibeiEmulationCreationError::Ashpd(Response(
ResponseError::Cancelled,
)))
) {
return true;
}
#[cfg(all(unix, feature = "remote_desktop_portal", not(target_os = "macos")))]
if matches!(
self,
EmulationCreationError::Xdp(XdpEmulationCreationError::Ashpd(Response(
ResponseError::Cancelled,
)))
) {
return true;
}
false
}
}
#[cfg(all(unix, feature = "wlroots", not(target_os = "macos")))]
#[derive(Debug, Error)]
pub enum WlrootsEmulationCreationError {
#[error(transparent)]
Connect(#[from] ConnectError),
#[error(transparent)]
Global(#[from] GlobalError),
#[error(transparent)]
Wayland(#[from] WaylandError),
#[error(transparent)]
Bind(#[from] WaylandBindError),
#[error(transparent)]
Dispatch(#[from] DispatchError),
#[error(transparent)]
Io(#[from] std::io::Error),
}
#[cfg(all(unix, feature = "wlroots", not(target_os = "macos")))]
#[derive(Debug, Error)]
#[error("wayland protocol \"{protocol}\" not supported: {inner}")]
pub struct WaylandBindError {
inner: BindError,
protocol: &'static str,
}
#[cfg(all(unix, feature = "wlroots", not(target_os = "macos")))]
impl WaylandBindError {
pub(crate) fn new(inner: BindError, protocol: &'static str) -> Self {
Self { inner, protocol }
}
}
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
#[derive(Debug, Error)]
pub enum LibeiEmulationCreationError {
#[error(transparent)]
Ashpd(#[from] ashpd::Error),
#[error(transparent)]
Io(#[from] std::io::Error),
#[error(transparent)]
Reis(#[from] reis::Error),
}
#[cfg(all(unix, feature = "remote_desktop_portal", not(target_os = "macos")))]
#[derive(Debug, Error)]
pub enum XdpEmulationCreationError {
#[error(transparent)]
Ashpd(#[from] ashpd::Error),
}
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
#[derive(Debug, Error)]
pub enum X11EmulationCreationError {
#[error("could not open display")]
OpenDisplay,
}
#[cfg(target_os = "macos")]
#[derive(Debug, Error)]
pub enum MacOSEmulationCreationError {
#[error("could not create event source")]
EventSourceCreation,
}
#[cfg(windows)]
#[derive(Debug, Error)]
pub enum WindowsEmulationCreationError {}

240
input-emulation/src/lib.rs Normal file
View File

@@ -0,0 +1,240 @@
use async_trait::async_trait;
use std::{
collections::{HashMap, HashSet},
fmt::Display,
};
use input_event::{Event, KeyboardEvent};
pub use self::error::{EmulationCreationError, EmulationError, InputEmulationError};
#[cfg(windows)]
mod windows;
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
mod x11;
#[cfg(all(unix, feature = "wlroots", not(target_os = "macos")))]
mod wlroots;
#[cfg(all(unix, feature = "remote_desktop_portal", not(target_os = "macos")))]
mod xdg_desktop_portal;
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
mod libei;
#[cfg(target_os = "macos")]
mod macos;
/// fallback input emulation (logs events)
mod dummy;
mod error;
pub type EmulationHandle = u64;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Backend {
#[cfg(all(unix, feature = "wlroots", not(target_os = "macos")))]
Wlroots,
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
Libei,
#[cfg(all(unix, feature = "remote_desktop_portal", not(target_os = "macos")))]
Xdp,
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
X11,
#[cfg(windows)]
Windows,
#[cfg(target_os = "macos")]
MacOs,
Dummy,
}
impl Display for Backend {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
#[cfg(all(unix, feature = "wlroots", not(target_os = "macos")))]
Backend::Wlroots => write!(f, "wlroots"),
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
Backend::Libei => write!(f, "libei"),
#[cfg(all(unix, feature = "remote_desktop_portal", not(target_os = "macos")))]
Backend::Xdp => write!(f, "xdg-desktop-portal"),
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
Backend::X11 => write!(f, "X11"),
#[cfg(windows)]
Backend::Windows => write!(f, "windows"),
#[cfg(target_os = "macos")]
Backend::MacOs => write!(f, "macos"),
Backend::Dummy => write!(f, "dummy"),
}
}
}
pub struct InputEmulation {
emulation: Box<dyn Emulation>,
handles: HashSet<EmulationHandle>,
pressed_keys: HashMap<EmulationHandle, HashSet<u32>>,
}
impl InputEmulation {
async fn with_backend(backend: Backend) -> Result<InputEmulation, EmulationCreationError> {
let emulation: Box<dyn Emulation> = match backend {
#[cfg(all(unix, feature = "wlroots", not(target_os = "macos")))]
Backend::Wlroots => Box::new(wlroots::WlrootsEmulation::new()?),
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
Backend::Libei => Box::new(libei::LibeiEmulation::new().await?),
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
Backend::X11 => Box::new(x11::X11Emulation::new()?),
#[cfg(all(unix, feature = "remote_desktop_portal", not(target_os = "macos")))]
Backend::Xdp => Box::new(xdg_desktop_portal::DesktopPortalEmulation::new().await?),
#[cfg(windows)]
Backend::Windows => Box::new(windows::WindowsEmulation::new()?),
#[cfg(target_os = "macos")]
Backend::MacOs => Box::new(macos::MacOSEmulation::new()?),
Backend::Dummy => Box::new(dummy::DummyEmulation::new()),
};
Ok(Self {
emulation,
handles: HashSet::new(),
pressed_keys: HashMap::new(),
})
}
pub async fn new(backend: Option<Backend>) -> Result<InputEmulation, EmulationCreationError> {
if let Some(backend) = backend {
let b = Self::with_backend(backend).await;
if b.is_ok() {
log::info!("using emulation backend: {backend}");
}
return b;
}
for backend in [
#[cfg(all(unix, feature = "wlroots", not(target_os = "macos")))]
Backend::Wlroots,
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
Backend::Libei,
#[cfg(all(unix, feature = "remote_desktop_portal", not(target_os = "macos")))]
Backend::Xdp,
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
Backend::X11,
#[cfg(windows)]
Backend::Windows,
#[cfg(target_os = "macos")]
Backend::MacOs,
Backend::Dummy,
] {
match Self::with_backend(backend).await {
Ok(b) => {
log::info!("using emulation backend: {backend}");
return Ok(b);
}
Err(e) if e.cancelled_by_user() => return Err(e),
Err(e) => log::warn!("{e}"),
}
}
Err(EmulationCreationError::NoAvailableBackend)
}
pub async fn consume(
&mut self,
event: Event,
handle: EmulationHandle,
) -> Result<(), EmulationError> {
match event {
Event::Keyboard(KeyboardEvent::Key { key, state, .. }) => {
// prevent double pressed / released keys
if self.update_pressed_keys(handle, key, state) {
self.emulation.consume(event, handle).await?;
}
Ok(())
}
_ => self.emulation.consume(event, handle).await,
}
}
pub async fn create(&mut self, handle: EmulationHandle) -> bool {
if self.handles.insert(handle) {
self.pressed_keys.insert(handle, HashSet::new());
self.emulation.create(handle).await;
true
} else {
false
}
}
pub async fn destroy(&mut self, handle: EmulationHandle) {
let _ = self.release_keys(handle).await;
if self.handles.remove(&handle) {
self.pressed_keys.remove(&handle);
self.emulation.destroy(handle).await
}
}
pub async fn terminate(&mut self) {
for handle in self.handles.iter().cloned().collect::<Vec<_>>() {
self.destroy(handle).await
}
self.emulation.terminate().await
}
pub async fn release_keys(&mut self, handle: EmulationHandle) -> Result<(), EmulationError> {
if let Some(keys) = self.pressed_keys.get_mut(&handle) {
let keys = keys.drain().collect::<Vec<_>>();
for key in keys {
let event = Event::Keyboard(KeyboardEvent::Key {
time: 0,
key,
state: 0,
});
self.emulation.consume(event, handle).await?;
if let Ok(key) = input_event::scancode::Linux::try_from(key) {
log::warn!("releasing stuck key: {key:?}");
}
}
}
let event = Event::Keyboard(KeyboardEvent::Modifiers {
depressed: 0,
latched: 0,
locked: 0,
group: 0,
});
self.emulation.consume(event, handle).await?;
Ok(())
}
pub fn has_pressed_keys(&self, handle: EmulationHandle) -> bool {
self.pressed_keys
.get(&handle)
.is_some_and(|p| !p.is_empty())
}
/// update the pressed_keys for the given handle
/// returns whether the event should be processed
fn update_pressed_keys(&mut self, handle: EmulationHandle, key: u32, state: u8) -> bool {
let Some(pressed_keys) = self.pressed_keys.get_mut(&handle) else {
return false;
};
if state == 0 {
// currently pressed => can release
pressed_keys.remove(&key)
} else {
// currently not pressed => can press
pressed_keys.insert(key)
}
}
}
#[async_trait]
trait Emulation: Send {
async fn consume(
&mut self,
event: Event,
handle: EmulationHandle,
) -> Result<(), EmulationError>;
async fn create(&mut self, handle: EmulationHandle);
async fn destroy(&mut self, handle: EmulationHandle);
async fn terminate(&mut self);
}

View File

@@ -0,0 +1,336 @@
use futures::{future, StreamExt};
use std::{
io,
os::{fd::OwnedFd, unix::net::UnixStream},
sync::{
atomic::{AtomicBool, Ordering},
Arc, Mutex, RwLock,
},
time::{SystemTime, UNIX_EPOCH},
};
use tokio::task::JoinHandle;
use ashpd::desktop::{
remote_desktop::{DeviceType, RemoteDesktop},
PersistMode, Session,
};
use async_trait::async_trait;
use reis::{
ei::{
self, button::ButtonState, handshake::ContextType, keyboard::KeyState, Button, Keyboard,
Pointer, Scroll,
},
event::{self, Connection, DeviceCapability, DeviceEvent, EiEvent, SeatEvent},
tokio::EiConvertEventStream,
};
use input_event::{Event, KeyboardEvent, PointerEvent};
use crate::error::EmulationError;
use super::{error::LibeiEmulationCreationError, Emulation, EmulationHandle};
#[derive(Clone, Default)]
struct Devices {
pointer: Arc<RwLock<Option<(ei::Device, ei::Pointer)>>>,
scroll: Arc<RwLock<Option<(ei::Device, ei::Scroll)>>>,
button: Arc<RwLock<Option<(ei::Device, ei::Button)>>>,
keyboard: Arc<RwLock<Option<(ei::Device, ei::Keyboard)>>>,
}
pub(crate) struct LibeiEmulation<'a> {
context: ei::Context,
conn: event::Connection,
devices: Devices,
ei_task: JoinHandle<()>,
error: Arc<Mutex<Option<EmulationError>>>,
libei_error: Arc<AtomicBool>,
_remote_desktop: RemoteDesktop<'a>,
session: Session<'a, RemoteDesktop<'a>>,
}
async fn get_ei_fd<'a>(
) -> Result<(RemoteDesktop<'a>, Session<'a, RemoteDesktop<'a>>, OwnedFd), ashpd::Error> {
let remote_desktop = RemoteDesktop::new().await?;
log::debug!("creating session ...");
let session = remote_desktop.create_session().await?;
log::debug!("selecting devices ...");
remote_desktop
.select_devices(
&session,
DeviceType::Keyboard | DeviceType::Pointer,
None,
PersistMode::ExplicitlyRevoked,
)
.await?;
log::info!("requesting permission for input emulation");
let _devices = remote_desktop.start(&session, None).await?.response()?;
let fd = remote_desktop.connect_to_eis(&session).await?;
Ok((remote_desktop, session, fd))
}
impl<'a> LibeiEmulation<'a> {
pub(crate) async fn new() -> Result<Self, LibeiEmulationCreationError> {
let (_remote_desktop, session, eifd) = get_ei_fd().await?;
let stream = UnixStream::from(eifd);
stream.set_nonblocking(true)?;
let context = ei::Context::new(stream)?;
let (conn, events) = context
.handshake_tokio("de.feschber.LanMouse", ContextType::Sender)
.await?;
let devices = Devices::default();
let libei_error = Arc::new(AtomicBool::default());
let error = Arc::new(Mutex::new(None));
let ei_handler = ei_task(
events,
conn.clone(),
context.clone(),
devices.clone(),
libei_error.clone(),
error.clone(),
);
let ei_task = tokio::task::spawn_local(ei_handler);
Ok(Self {
context,
conn,
devices,
ei_task,
error,
libei_error,
_remote_desktop,
session,
})
}
}
impl<'a> Drop for LibeiEmulation<'a> {
fn drop(&mut self) {
self.ei_task.abort();
}
}
#[async_trait]
impl<'a> Emulation for LibeiEmulation<'a> {
async fn consume(
&mut self,
event: Event,
_handle: EmulationHandle,
) -> Result<(), EmulationError> {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_micros() as u64;
if self.libei_error.load(Ordering::SeqCst) {
// don't break sending additional events but signal error
if let Some(e) = self.error.lock().unwrap().take() {
return Err(e);
}
}
match event {
Event::Pointer(p) => match p {
PointerEvent::Motion { time: _, dx, dy } => {
let pointer_device = self.devices.pointer.read().unwrap();
if let Some((d, p)) = pointer_device.as_ref() {
p.motion_relative(dx as f32, dy as f32);
d.frame(self.conn.serial(), now);
}
}
PointerEvent::Button {
time: _,
button,
state,
} => {
let button_device = self.devices.button.read().unwrap();
if let Some((d, b)) = button_device.as_ref() {
b.button(
button,
match state {
0 => ButtonState::Released,
_ => ButtonState::Press,
},
);
d.frame(self.conn.serial(), now);
}
}
PointerEvent::Axis {
time: _,
axis,
value,
} => {
let scroll_device = self.devices.scroll.read().unwrap();
if let Some((d, s)) = scroll_device.as_ref() {
match axis {
0 => s.scroll(0., value as f32),
_ => s.scroll(value as f32, 0.),
}
d.frame(self.conn.serial(), now);
}
}
PointerEvent::AxisDiscrete120 { axis, value } => {
let scroll_device = self.devices.scroll.read().unwrap();
if let Some((d, s)) = scroll_device.as_ref() {
match axis {
0 => s.scroll_discrete(0, value),
_ => s.scroll_discrete(value, 0),
}
d.frame(self.conn.serial(), now);
}
}
},
Event::Keyboard(k) => match k {
KeyboardEvent::Key {
time: _,
key,
state,
} => {
let keyboard_device = self.devices.keyboard.read().unwrap();
if let Some((d, k)) = keyboard_device.as_ref() {
k.key(
key,
match state {
0 => KeyState::Released,
_ => KeyState::Press,
},
);
d.frame(self.conn.serial(), now);
}
}
KeyboardEvent::Modifiers { .. } => {}
},
}
self.context
.flush()
.map_err(|e| io::Error::new(e.kind(), e))?;
Ok(())
}
async fn create(&mut self, _: EmulationHandle) {}
async fn destroy(&mut self, _: EmulationHandle) {}
async fn terminate(&mut self) {
let _ = self.session.close().await;
self.ei_task.abort();
}
}
async fn ei_task(
mut events: EiConvertEventStream,
_conn: Connection,
context: ei::Context,
devices: Devices,
libei_error: Arc<AtomicBool>,
error: Arc<Mutex<Option<EmulationError>>>,
) {
loop {
match ei_event_handler(&mut events, &context, &devices).await {
Ok(()) => {}
Err(e) => {
libei_error.store(true, Ordering::SeqCst);
error.lock().unwrap().replace(e);
// wait for termination -> otherwise we will loop forever
future::pending::<()>().await;
}
}
}
}
async fn ei_event_handler(
events: &mut EiConvertEventStream,
context: &ei::Context,
devices: &Devices,
) -> Result<(), EmulationError> {
loop {
let event = events.next().await.ok_or(EmulationError::EndOfStream)??;
const CAPABILITIES: &[DeviceCapability] = &[
DeviceCapability::Pointer,
DeviceCapability::PointerAbsolute,
DeviceCapability::Keyboard,
DeviceCapability::Touch,
DeviceCapability::Scroll,
DeviceCapability::Button,
];
log::debug!("{event:?}");
match event {
EiEvent::Disconnected(e) => {
log::debug!("ei disconnected: {e:?}");
return Err(EmulationError::EndOfStream);
}
EiEvent::SeatAdded(e) => {
e.seat().bind_capabilities(CAPABILITIES);
}
EiEvent::SeatRemoved(e) => {
log::debug!("seat removed: {:?}", e.seat());
}
EiEvent::DeviceAdded(e) => {
let device_type = e.device().device_type();
log::debug!("device added: {device_type:?}");
e.device().device();
let device = e.device();
if let Some(pointer) = e.device().interface::<Pointer>() {
devices
.pointer
.write()
.unwrap()
.replace((device.device().clone(), pointer));
}
if let Some(keyboard) = e.device().interface::<Keyboard>() {
devices
.keyboard
.write()
.unwrap()
.replace((device.device().clone(), keyboard));
}
if let Some(scroll) = e.device().interface::<Scroll>() {
devices
.scroll
.write()
.unwrap()
.replace((device.device().clone(), scroll));
}
if let Some(button) = e.device().interface::<Button>() {
devices
.button
.write()
.unwrap()
.replace((device.device().clone(), button));
}
}
EiEvent::DeviceRemoved(e) => {
log::debug!("device removed: {:?}", e.device().device_type());
}
EiEvent::DevicePaused(e) => {
log::debug!("device paused: {:?}", e.device().device_type());
}
EiEvent::DeviceResumed(e) => {
log::debug!("device resumed: {:?}", e.device().device_type());
e.device().device().start_emulating(0, 0);
}
EiEvent::KeyboardModifiers(e) => {
log::debug!("modifiers: {e:?}");
}
// only for receiver context
// EiEvent::Frame(_) => { },
// EiEvent::DeviceStartEmulating(_) => { },
// EiEvent::DeviceStopEmulating(_) => { },
// EiEvent::PointerMotion(_) => { },
// EiEvent::PointerMotionAbsolute(_) => { },
// EiEvent::Button(_) => { },
// EiEvent::ScrollDelta(_) => { },
// EiEvent::ScrollStop(_) => { },
// EiEvent::ScrollCancel(_) => { },
// EiEvent::ScrollDiscrete(_) => { },
// EiEvent::KeyboardKey(_) => { },
// EiEvent::TouchDown(_) => { },
// EiEvent::TouchUp(_) => { },
// EiEvent::TouchMotion(_) => { },
_ => unreachable!("unexpected ei event"),
}
context.flush().map_err(|e| io::Error::new(e.kind(), e))?;
}
}

View File

@@ -0,0 +1,495 @@
use super::{error::EmulationError, Emulation, EmulationHandle};
use async_trait::async_trait;
use bitflags::bitflags;
use core_graphics::base::CGFloat;
use core_graphics::display::{
CGDirectDisplayID, CGDisplayBounds, CGGetDisplaysWithRect, CGPoint, CGRect, CGSize,
};
use core_graphics::event::{
CGEvent, CGEventFlags, CGEventTapLocation, CGEventType, CGKeyCode, CGMouseButton, EventField,
ScrollEventUnit,
};
use core_graphics::event_source::{CGEventSource, CGEventSourceStateID};
use input_event::{scancode, Event, KeyboardEvent, PointerEvent};
use keycode::{KeyMap, KeyMapping};
use std::cell::Cell;
use std::ops::{Index, IndexMut};
use std::rc::Rc;
use std::sync::Arc;
use std::time::Duration;
use tokio::{sync::Notify, task::JoinHandle};
use super::error::MacOSEmulationCreationError;
const DEFAULT_REPEAT_DELAY: Duration = Duration::from_millis(500);
const DEFAULT_REPEAT_INTERVAL: Duration = Duration::from_millis(32);
pub(crate) struct MacOSEmulation {
event_source: CGEventSource,
repeat_task: Option<JoinHandle<()>>,
button_state: ButtonState,
modifier_state: Rc<Cell<XMods>>,
notify_repeat_task: Arc<Notify>,
}
struct ButtonState {
left: bool,
right: bool,
center: bool,
}
impl Index<CGMouseButton> for ButtonState {
type Output = bool;
fn index(&self, index: CGMouseButton) -> &Self::Output {
match index {
CGMouseButton::Left => &self.left,
CGMouseButton::Right => &self.right,
CGMouseButton::Center => &self.center,
}
}
}
impl IndexMut<CGMouseButton> for ButtonState {
fn index_mut(&mut self, index: CGMouseButton) -> &mut Self::Output {
match index {
CGMouseButton::Left => &mut self.left,
CGMouseButton::Right => &mut self.right,
CGMouseButton::Center => &mut self.center,
}
}
}
unsafe impl Send for MacOSEmulation {}
impl MacOSEmulation {
pub(crate) fn new() -> Result<Self, MacOSEmulationCreationError> {
let event_source = CGEventSource::new(CGEventSourceStateID::CombinedSessionState)
.map_err(|_| MacOSEmulationCreationError::EventSourceCreation)?;
let button_state = ButtonState {
left: false,
right: false,
center: false,
};
Ok(Self {
event_source,
button_state,
repeat_task: None,
notify_repeat_task: Arc::new(Notify::new()),
modifier_state: Rc::new(Cell::new(XMods::empty())),
})
}
fn get_mouse_location(&self) -> Option<CGPoint> {
let event: CGEvent = CGEvent::new(self.event_source.clone()).ok()?;
Some(event.location())
}
async fn spawn_repeat_task(&mut self, key: u16) {
// there can only be one repeating key and it's
// always the last to be pressed
self.cancel_repeat_task().await;
let event_source = self.event_source.clone();
let notify = self.notify_repeat_task.clone();
let modifiers = self.modifier_state.clone();
let repeat_task = tokio::task::spawn_local(async move {
let stop = tokio::select! {
_ = tokio::time::sleep(DEFAULT_REPEAT_DELAY) => false,
_ = notify.notified() => true,
};
if !stop {
loop {
key_event(event_source.clone(), key, 1, modifiers.get());
tokio::select! {
_ = tokio::time::sleep(DEFAULT_REPEAT_INTERVAL) => {},
_ = notify.notified() => break,
}
}
}
// release key when cancelled
update_modifiers(&modifiers, key as u32, 0);
key_event(event_source.clone(), key, 0, modifiers.get());
});
self.repeat_task = Some(repeat_task);
}
async fn cancel_repeat_task(&mut self) {
if let Some(task) = self.repeat_task.take() {
self.notify_repeat_task.notify_waiters();
let _ = task.await;
}
}
}
fn key_event(event_source: CGEventSource, key: u16, state: u8, modifiers: XMods) {
let event = match CGEvent::new_keyboard_event(event_source, key, state != 0) {
Ok(e) => e,
Err(_) => {
log::warn!("unable to create key event");
return;
}
};
event.set_flags(to_cgevent_flags(modifiers));
event.post(CGEventTapLocation::HID);
log::trace!("key event: {key} {state}");
}
fn modifier_event(event_source: CGEventSource, depressed: XMods) {
let Ok(event) = CGEvent::new(event_source) else {
log::warn!("could not create CGEvent");
return;
};
let flags = to_cgevent_flags(depressed);
event.set_type(CGEventType::FlagsChanged);
event.set_flags(flags);
event.post(CGEventTapLocation::HID);
log::trace!("modifiers updated: {depressed:?}");
}
fn get_display_at_point(x: CGFloat, y: CGFloat) -> Option<CGDirectDisplayID> {
let mut displays: [CGDirectDisplayID; 16] = [0; 16];
let mut display_count: u32 = 0;
let rect = CGRect::new(&CGPoint::new(x, y), &CGSize::new(0.0, 0.0));
let error = unsafe {
CGGetDisplaysWithRect(
rect,
1,
displays.as_mut_ptr(),
&mut display_count as *mut u32,
)
};
if error != 0 {
log::warn!("error getting displays at point ({}, {}): {}", x, y, error);
return Option::None;
}
if display_count == 0 {
log::debug!("no displays found at point ({}, {})", x, y);
return Option::None;
}
return displays.first().copied();
}
fn get_display_bounds(display: CGDirectDisplayID) -> (CGFloat, CGFloat, CGFloat, CGFloat) {
unsafe {
let bounds = CGDisplayBounds(display);
let min_x = bounds.origin.x;
let max_x = bounds.origin.x + bounds.size.width;
let min_y = bounds.origin.y;
let max_y = bounds.origin.y + bounds.size.height;
(min_x as f64, min_y as f64, max_x as f64, max_y as f64)
}
}
fn clamp_to_screen_space(
current_x: CGFloat,
current_y: CGFloat,
dx: CGFloat,
dy: CGFloat,
) -> (CGFloat, CGFloat) {
// Check which display the mouse is currently on
// Determine what the location of the mouse would be after applying the move
// Get the display at the new location
// If the point is not on a display
// Clamp the mouse to the current display
// Else If the point is on a display
// Clamp the mouse to the new display
let current_display = match get_display_at_point(current_x, current_y) {
Some(display) => display,
None => {
log::warn!("could not get current display!");
return (current_x, current_y);
}
};
let new_x = current_x + dx;
let new_y = current_y + dy;
let final_display = get_display_at_point(new_x, new_y).unwrap_or(current_display);
let (min_x, min_y, max_x, max_y) = get_display_bounds(final_display);
(
new_x.clamp(min_x, max_x - 1.),
new_y.clamp(min_y, max_y - 1.),
)
}
#[async_trait]
impl Emulation for MacOSEmulation {
async fn consume(
&mut self,
event: Event,
_handle: EmulationHandle,
) -> Result<(), EmulationError> {
match event {
Event::Pointer(pointer_event) => match pointer_event {
PointerEvent::Motion { time: _, dx, dy } => {
let mut mouse_location = match self.get_mouse_location() {
Some(l) => l,
None => {
log::warn!("could not get mouse location!");
return Ok(());
}
};
let (new_mouse_x, new_mouse_y) =
clamp_to_screen_space(mouse_location.x, mouse_location.y, dx, dy);
mouse_location.x = new_mouse_x;
mouse_location.y = new_mouse_y;
let mut event_type = CGEventType::MouseMoved;
if self.button_state.left {
event_type = CGEventType::LeftMouseDragged
} else if self.button_state.right {
event_type = CGEventType::RightMouseDragged
} else if self.button_state.center {
event_type = CGEventType::OtherMouseDragged
};
let event = match CGEvent::new_mouse_event(
self.event_source.clone(),
event_type,
mouse_location,
CGMouseButton::Left,
) {
Ok(e) => e,
Err(_) => {
log::warn!("mouse event creation failed!");
return Ok(());
}
};
event.set_integer_value_field(EventField::MOUSE_EVENT_DELTA_X, dx as i64);
event.set_integer_value_field(EventField::MOUSE_EVENT_DELTA_Y, dy as i64);
event.post(CGEventTapLocation::HID);
}
PointerEvent::Button {
time: _,
button,
state,
} => {
let (event_type, mouse_button) = match (button, state) {
(b, 1) if b == input_event::BTN_LEFT => {
(CGEventType::LeftMouseDown, CGMouseButton::Left)
}
(b, 0) if b == input_event::BTN_LEFT => {
(CGEventType::LeftMouseUp, CGMouseButton::Left)
}
(b, 1) if b == input_event::BTN_RIGHT => {
(CGEventType::RightMouseDown, CGMouseButton::Right)
}
(b, 0) if b == input_event::BTN_RIGHT => {
(CGEventType::RightMouseUp, CGMouseButton::Right)
}
(b, 1) if b == input_event::BTN_MIDDLE => {
(CGEventType::OtherMouseDown, CGMouseButton::Center)
}
(b, 0) if b == input_event::BTN_MIDDLE => {
(CGEventType::OtherMouseUp, CGMouseButton::Center)
}
_ => {
log::warn!("invalid button event: {button},{state}");
return Ok(());
}
};
// store button state
self.button_state[mouse_button] = state == 1;
let location = self.get_mouse_location().unwrap();
let event = match CGEvent::new_mouse_event(
self.event_source.clone(),
event_type,
location,
mouse_button,
) {
Ok(e) => e,
Err(()) => {
log::warn!("mouse event creation failed!");
return Ok(());
}
};
event.post(CGEventTapLocation::HID);
}
PointerEvent::Axis {
time: _,
axis,
value,
} => {
let value = value as i32;
let (count, wheel1, wheel2, wheel3) = match axis {
0 => (1, value, 0, 0), // 0 = vertical => 1 scroll wheel device (y axis)
1 => (2, 0, value, 0), // 1 = horizontal => 2 scroll wheel devices (y, x) -> (0, x)
_ => {
log::warn!("invalid scroll event: {axis}, {value}");
return Ok(());
}
};
let event = match CGEvent::new_scroll_event(
self.event_source.clone(),
ScrollEventUnit::PIXEL,
count,
wheel1,
wheel2,
wheel3,
) {
Ok(e) => e,
Err(()) => {
log::warn!("scroll event creation failed!");
return Ok(());
}
};
event.post(CGEventTapLocation::HID);
}
PointerEvent::AxisDiscrete120 { axis, value } => {
let (count, wheel1, wheel2, wheel3) = match axis {
0 => (1, value, 0, 0), // 0 = vertical => 1 scroll wheel device (y axis)
1 => (2, 0, value, 0), // 1 = horizontal => 2 scroll wheel devices (y, x) -> (0, x)
_ => {
log::warn!("invalid scroll event: {axis}, {value}");
return Ok(());
}
};
let event = match CGEvent::new_scroll_event(
self.event_source.clone(),
ScrollEventUnit::PIXEL,
count,
wheel1,
wheel2,
wheel3,
) {
Ok(e) => e,
Err(()) => {
log::warn!("scroll event creation failed!");
return Ok(());
}
};
event.post(CGEventTapLocation::HID);
}
},
Event::Keyboard(keyboard_event) => match keyboard_event {
KeyboardEvent::Key {
time: _,
key,
state,
} => {
let code = match KeyMap::from_key_mapping(KeyMapping::Evdev(key as u16)) {
Ok(k) => k.mac as CGKeyCode,
Err(_) => {
log::warn!("unable to map key event");
return Ok(());
}
};
match state {
// pressed
1 => self.spawn_repeat_task(code).await,
_ => self.cancel_repeat_task().await,
}
update_modifiers(&self.modifier_state, key, state);
key_event(
self.event_source.clone(),
code,
state,
self.modifier_state.get(),
);
}
KeyboardEvent::Modifiers {
depressed,
latched,
locked,
group,
} => {
set_modifiers(&self.modifier_state, depressed, latched, locked, group);
modifier_event(self.event_source.clone(), self.modifier_state.get());
}
},
}
// FIXME
Ok(())
}
async fn create(&mut self, _handle: EmulationHandle) {}
async fn destroy(&mut self, _handle: EmulationHandle) {}
async fn terminate(&mut self) {}
}
fn update_modifiers(modifiers: &Cell<XMods>, key: u32, state: u8) -> bool {
if let Ok(key) = scancode::Linux::try_from(key) {
let mask = match key {
scancode::Linux::KeyLeftShift | scancode::Linux::KeyRightShift => XMods::ShiftMask,
scancode::Linux::KeyCapsLock => XMods::LockMask,
scancode::Linux::KeyLeftCtrl | scancode::Linux::KeyRightCtrl => XMods::ControlMask,
scancode::Linux::KeyLeftAlt | scancode::Linux::KeyRightalt => XMods::Mod1Mask,
scancode::Linux::KeyLeftMeta | scancode::Linux::KeyRightmeta => XMods::Mod4Mask,
_ => XMods::empty(),
};
// unchanged
if mask.is_empty() {
return false;
}
let mut mods = modifiers.get();
match state {
1 => mods.insert(mask),
_ => mods.remove(mask),
}
modifiers.set(mods);
true
} else {
false
}
}
fn set_modifiers(
active_modifiers: &Cell<XMods>,
depressed: u32,
latched: u32,
locked: u32,
group: u32,
) {
let depressed = XMods::from_bits(depressed).unwrap_or_default();
let _latched = XMods::from_bits(latched).unwrap_or_default();
let _locked = XMods::from_bits(locked).unwrap_or_default();
let _group = XMods::from_bits(group).unwrap_or_default();
// we only care about the depressed modifiers for now
active_modifiers.replace(depressed);
}
fn to_cgevent_flags(depressed: XMods) -> CGEventFlags {
let mut flags = CGEventFlags::empty();
if depressed.contains(XMods::ShiftMask) {
flags |= CGEventFlags::CGEventFlagShift;
}
if depressed.contains(XMods::LockMask) {
flags |= CGEventFlags::CGEventFlagAlphaShift;
}
if depressed.contains(XMods::ControlMask) {
flags |= CGEventFlags::CGEventFlagControl;
}
if depressed.contains(XMods::Mod1Mask) {
flags |= CGEventFlags::CGEventFlagAlternate;
}
if depressed.contains(XMods::Mod4Mask) {
flags |= CGEventFlags::CGEventFlagCommand;
}
flags
}
// From X11/X.h
bitflags! {
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
struct XMods: u32 {
const ShiftMask = (1<<0);
const LockMask = (1<<1);
const ControlMask = (1<<2);
const Mod1Mask = (1<<3);
const Mod2Mask = (1<<4);
const Mod3Mask = (1<<5);
const Mod4Mask = (1<<6);
const Mod5Mask = (1<<7);
}
}

View File

@@ -0,0 +1,237 @@
use super::error::{EmulationError, WindowsEmulationCreationError};
use input_event::{
scancode, Event, KeyboardEvent, PointerEvent, BTN_BACK, BTN_FORWARD, BTN_LEFT, BTN_MIDDLE,
BTN_RIGHT,
};
use async_trait::async_trait;
use std::ops::BitOrAssign;
use std::time::Duration;
use tokio::task::AbortHandle;
use windows::Win32::UI::Input::KeyboardAndMouse::{
SendInput, INPUT_0, KEYEVENTF_EXTENDEDKEY, MOUSEEVENTF_XDOWN, MOUSEEVENTF_XUP,
};
use windows::Win32::UI::Input::KeyboardAndMouse::{
INPUT, INPUT_KEYBOARD, INPUT_MOUSE, KEYBDINPUT, KEYEVENTF_KEYUP, KEYEVENTF_SCANCODE,
MOUSEEVENTF_HWHEEL, MOUSEEVENTF_LEFTDOWN, MOUSEEVENTF_LEFTUP, MOUSEEVENTF_MIDDLEDOWN,
MOUSEEVENTF_MIDDLEUP, MOUSEEVENTF_MOVE, MOUSEEVENTF_RIGHTDOWN, MOUSEEVENTF_RIGHTUP,
MOUSEEVENTF_WHEEL, MOUSEINPUT,
};
use windows::Win32::UI::WindowsAndMessaging::{XBUTTON1, XBUTTON2};
use super::{Emulation, EmulationHandle};
const DEFAULT_REPEAT_DELAY: Duration = Duration::from_millis(500);
const DEFAULT_REPEAT_INTERVAL: Duration = Duration::from_millis(32);
pub(crate) struct WindowsEmulation {
repeat_task: Option<AbortHandle>,
}
impl WindowsEmulation {
pub(crate) fn new() -> Result<Self, WindowsEmulationCreationError> {
Ok(Self { repeat_task: None })
}
}
#[async_trait]
impl Emulation for WindowsEmulation {
async fn consume(&mut self, event: Event, _: EmulationHandle) -> Result<(), EmulationError> {
match event {
Event::Pointer(pointer_event) => match pointer_event {
PointerEvent::Motion { time: _, dx, dy } => {
rel_mouse(dx as i32, dy as i32);
}
PointerEvent::Button {
time: _,
button,
state,
} => mouse_button(button, state),
PointerEvent::Axis {
time: _,
axis,
value,
} => scroll(axis, value as i32),
PointerEvent::AxisDiscrete120 { axis, value } => scroll(axis, value),
},
Event::Keyboard(keyboard_event) => match keyboard_event {
KeyboardEvent::Key {
time: _,
key,
state,
} => {
match state {
// pressed
0 => self.kill_repeat_task(),
1 => self.spawn_repeat_task(key).await,
_ => {}
}
key_event(key, state)
}
KeyboardEvent::Modifiers { .. } => {}
},
}
// FIXME
Ok(())
}
async fn create(&mut self, _handle: EmulationHandle) {}
async fn destroy(&mut self, _handle: EmulationHandle) {}
async fn terminate(&mut self) {}
}
impl WindowsEmulation {
async fn spawn_repeat_task(&mut self, key: u32) {
// there can only be one repeating key and it's
// always the last to be pressed
self.kill_repeat_task();
let repeat_task = tokio::task::spawn_local(async move {
tokio::time::sleep(DEFAULT_REPEAT_DELAY).await;
loop {
key_event(key, 1);
tokio::time::sleep(DEFAULT_REPEAT_INTERVAL).await;
}
});
self.repeat_task = Some(repeat_task.abort_handle());
}
fn kill_repeat_task(&mut self) {
if let Some(task) = self.repeat_task.take() {
task.abort();
}
}
}
fn send_input_safe(input: INPUT) {
unsafe {
loop {
/* retval = number of successfully submitted events */
if SendInput(&[input], std::mem::size_of::<INPUT>() as i32) > 0 {
break;
}
}
}
}
fn send_mouse_input(mi: MOUSEINPUT) {
send_input_safe(INPUT {
r#type: INPUT_MOUSE,
Anonymous: INPUT_0 { mi },
});
}
fn send_keyboard_input(ki: KEYBDINPUT) {
send_input_safe(INPUT {
r#type: INPUT_KEYBOARD,
Anonymous: INPUT_0 { ki },
});
}
fn rel_mouse(dx: i32, dy: i32) {
let mi = MOUSEINPUT {
dx,
dy,
mouseData: 0,
dwFlags: MOUSEEVENTF_MOVE,
time: 0,
dwExtraInfo: 0,
};
send_mouse_input(mi);
}
fn mouse_button(button: u32, state: u32) {
let dw_flags = match state {
0 => match button {
BTN_LEFT => MOUSEEVENTF_LEFTUP,
BTN_RIGHT => MOUSEEVENTF_RIGHTUP,
BTN_MIDDLE => MOUSEEVENTF_MIDDLEUP,
BTN_BACK => MOUSEEVENTF_XUP,
BTN_FORWARD => MOUSEEVENTF_XUP,
_ => return,
},
1 => match button {
BTN_LEFT => MOUSEEVENTF_LEFTDOWN,
BTN_RIGHT => MOUSEEVENTF_RIGHTDOWN,
BTN_MIDDLE => MOUSEEVENTF_MIDDLEDOWN,
BTN_BACK => MOUSEEVENTF_XDOWN,
BTN_FORWARD => MOUSEEVENTF_XDOWN,
_ => return,
},
_ => return,
};
let mouse_data = match button {
BTN_BACK => XBUTTON1 as u32,
BTN_FORWARD => XBUTTON2 as u32,
_ => 0,
};
let mi = MOUSEINPUT {
dx: 0,
dy: 0, // no movement
mouseData: mouse_data,
dwFlags: dw_flags,
time: 0,
dwExtraInfo: 0,
};
send_mouse_input(mi);
}
fn scroll(axis: u8, value: i32) {
let event_type = match axis {
0 => MOUSEEVENTF_WHEEL,
1 => MOUSEEVENTF_HWHEEL,
_ => return,
};
let mi = MOUSEINPUT {
dx: 0,
dy: 0,
mouseData: -value as u32,
dwFlags: event_type,
time: 0,
dwExtraInfo: 0,
};
send_mouse_input(mi);
}
fn key_event(key: u32, state: u8) {
let scancode = match linux_keycode_to_windows_scancode(key) {
Some(code) => code,
None => return,
};
let extended = scancode > 0xff;
let scancode = scancode & 0xff;
let mut flags = KEYEVENTF_SCANCODE;
if extended {
flags.bitor_assign(KEYEVENTF_EXTENDEDKEY);
}
if state == 0 {
flags.bitor_assign(KEYEVENTF_KEYUP);
}
let ki = KEYBDINPUT {
wVk: Default::default(),
wScan: scancode,
dwFlags: flags,
time: 0,
dwExtraInfo: 0,
};
send_keyboard_input(ki);
}
fn linux_keycode_to_windows_scancode(linux_keycode: u32) -> Option<u16> {
let linux_scancode = match scancode::Linux::try_from(linux_keycode) {
Ok(s) => s,
Err(_) => {
log::warn!("unknown keycode: {linux_keycode}");
return None;
}
};
log::trace!("linux code: {linux_scancode:?}");
let windows_scancode = match scancode::Windows::try_from(linux_scancode) {
Ok(s) => s,
Err(_) => {
log::warn!("failed to translate linux code into windows scancode: {linux_scancode:?}");
return None;
}
};
log::trace!("windows code: {windows_scancode:?}");
Some(windows_scancode as u16)
}

View File

@@ -0,0 +1,275 @@
use crate::error::EmulationError;
use super::{error::WlrootsEmulationCreationError, Emulation};
use async_trait::async_trait;
use std::collections::HashMap;
use std::io;
use std::os::fd::{AsFd, OwnedFd};
use wayland_client::backend::WaylandError;
use wayland_client::WEnum;
use wayland_client::protocol::wl_keyboard::{self, WlKeyboard};
use wayland_client::protocol::wl_pointer::{Axis, ButtonState};
use wayland_client::protocol::wl_seat::WlSeat;
use wayland_protocols_wlr::virtual_pointer::v1::client::{
zwlr_virtual_pointer_manager_v1::ZwlrVirtualPointerManagerV1 as VpManager,
zwlr_virtual_pointer_v1::ZwlrVirtualPointerV1 as Vp,
};
use wayland_protocols_misc::zwp_virtual_keyboard_v1::client::{
zwp_virtual_keyboard_manager_v1::ZwpVirtualKeyboardManagerV1 as VkManager,
zwp_virtual_keyboard_v1::ZwpVirtualKeyboardV1 as Vk,
};
use wayland_client::{
delegate_noop,
globals::{registry_queue_init, GlobalListContents},
protocol::{wl_registry, wl_seat},
Connection, Dispatch, EventQueue, QueueHandle,
};
use input_event::{Event, KeyboardEvent, PointerEvent};
use super::error::WaylandBindError;
use super::EmulationHandle;
struct State {
keymap: Option<(u32, OwnedFd, u32)>,
input_for_client: HashMap<EmulationHandle, VirtualInput>,
seat: wl_seat::WlSeat,
qh: QueueHandle<Self>,
vpm: VpManager,
vkm: VkManager,
}
// App State, implements Dispatch event handlers
pub(crate) struct WlrootsEmulation {
last_flush_failed: bool,
state: State,
queue: EventQueue<State>,
}
impl WlrootsEmulation {
pub(crate) fn new() -> Result<Self, WlrootsEmulationCreationError> {
let conn = Connection::connect_to_env()?;
let (globals, queue) = registry_queue_init::<State>(&conn)?;
let qh = queue.handle();
let seat: wl_seat::WlSeat = globals
.bind(&qh, 7..=8, ())
.map_err(|e| WaylandBindError::new(e, "wl_seat 7..=8"))?;
let vpm: VpManager = globals
.bind(&qh, 1..=1, ())
.map_err(|e| WaylandBindError::new(e, "wlr-virtual-pointer-unstable-v1"))?;
let vkm: VkManager = globals
.bind(&qh, 1..=1, ())
.map_err(|e| WaylandBindError::new(e, "virtual-keyboard-unstable-v1"))?;
let input_for_client: HashMap<EmulationHandle, VirtualInput> = HashMap::new();
let mut emulate = WlrootsEmulation {
last_flush_failed: false,
state: State {
keymap: None,
input_for_client,
seat,
vpm,
vkm,
qh,
},
queue,
};
while emulate.state.keymap.is_none() {
emulate.queue.blocking_dispatch(&mut emulate.state)?;
}
// let fd = unsafe { &File::from_raw_fd(emulate.state.keymap.unwrap().1.as_raw_fd()) };
// let mmap = unsafe { MmapOptions::new().map_copy(fd).unwrap() };
// log::debug!("{:?}", &mmap[..100]);
Ok(emulate)
}
}
impl State {
fn add_client(&mut self, client: EmulationHandle) {
let pointer: Vp = self.vpm.create_virtual_pointer(None, &self.qh, ());
let keyboard: Vk = self.vkm.create_virtual_keyboard(&self.seat, &self.qh, ());
// TODO: use server side keymap
if let Some((format, fd, size)) = self.keymap.as_ref() {
keyboard.keymap(*format, fd.as_fd(), *size);
} else {
panic!("no keymap");
}
let vinput = VirtualInput { pointer, keyboard };
self.input_for_client.insert(client, vinput);
}
fn destroy_client(&mut self, handle: EmulationHandle) {
if let Some(input) = self.input_for_client.remove(&handle) {
input.pointer.destroy();
input.keyboard.destroy();
}
}
}
#[async_trait]
impl Emulation for WlrootsEmulation {
async fn consume(
&mut self,
event: Event,
handle: EmulationHandle,
) -> Result<(), EmulationError> {
if let Some(virtual_input) = self.state.input_for_client.get(&handle) {
if self.last_flush_failed {
match self.queue.flush() {
Err(WaylandError::Io(e)) if e.kind() == io::ErrorKind::WouldBlock => {
/*
* outgoing buffer is full - sending more events
* will overwhelm the output buffer and leave the
* wayland connection in a broken state
*/
log::warn!("can't keep up, discarding event: ({handle}) - {event:?}");
return Ok(());
}
_ => {}
}
}
virtual_input
.consume_event(event)
.unwrap_or_else(|_| panic!("failed to convert event: {event:?}"));
match self.queue.flush() {
Err(WaylandError::Io(e)) if e.kind() == io::ErrorKind::WouldBlock => {
self.last_flush_failed = true;
log::warn!("can't keep up, discarding event: ({handle}) - {event:?}");
}
Err(WaylandError::Protocol(e)) => panic!("wayland protocol violation: {e}"),
Ok(()) => self.last_flush_failed = false,
Err(e) => Err(e)?,
}
}
Ok(())
}
async fn create(&mut self, handle: EmulationHandle) {
self.state.add_client(handle);
if let Err(e) = self.queue.flush() {
log::error!("{}", e);
}
}
async fn destroy(&mut self, handle: EmulationHandle) {
self.state.destroy_client(handle);
if let Err(e) = self.queue.flush() {
log::error!("{}", e);
}
}
async fn terminate(&mut self) {
/* nothing to do */
}
}
struct VirtualInput {
pointer: Vp,
keyboard: Vk,
}
impl VirtualInput {
fn consume_event(&self, event: Event) -> Result<(), ()> {
match event {
Event::Pointer(e) => {
match e {
PointerEvent::Motion { time, dx, dy } => self.pointer.motion(time, dx, dy),
PointerEvent::Button {
time,
button,
state,
} => {
let state: ButtonState = state.try_into()?;
self.pointer.button(time, button, state);
}
PointerEvent::Axis { time, axis, value } => {
let axis: Axis = (axis as u32).try_into()?;
self.pointer.axis(time, axis, value);
self.pointer.frame();
}
PointerEvent::AxisDiscrete120 { axis, value } => {
let axis: Axis = (axis as u32).try_into()?;
self.pointer
.axis_discrete(0, axis, value as f64 / 6., value / 120);
self.pointer.frame();
}
}
self.pointer.frame();
}
Event::Keyboard(e) => match e {
KeyboardEvent::Key { time, key, state } => {
self.keyboard.key(time, key, state as u32);
}
KeyboardEvent::Modifiers {
depressed: mods_depressed,
latched: mods_latched,
locked: mods_locked,
group,
} => {
self.keyboard
.modifiers(mods_depressed, mods_latched, mods_locked, group);
}
},
}
Ok(())
}
}
delegate_noop!(State: Vp);
delegate_noop!(State: Vk);
delegate_noop!(State: VpManager);
delegate_noop!(State: VkManager);
impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for State {
fn event(
_: &mut State,
_: &wl_registry::WlRegistry,
_: wl_registry::Event,
_: &GlobalListContents,
_: &Connection,
_: &QueueHandle<State>,
) {
}
}
impl Dispatch<WlKeyboard, ()> for State {
fn event(
state: &mut Self,
_: &WlKeyboard,
event: <WlKeyboard as wayland_client::Proxy>::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
if let wl_keyboard::Event::Keymap { format, fd, size } = event {
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, ());
}
}
}
}

154
input-emulation/src/x11.rs Normal file
View File

@@ -0,0 +1,154 @@
use async_trait::async_trait;
use std::ptr;
use x11::{
xlib::{self, XCloseDisplay},
xtest,
};
use input_event::{
Event, KeyboardEvent, PointerEvent, BTN_BACK, BTN_FORWARD, BTN_LEFT, BTN_MIDDLE, BTN_RIGHT,
};
use crate::error::EmulationError;
use super::{error::X11EmulationCreationError, Emulation, EmulationHandle};
pub(crate) struct X11Emulation {
display: *mut xlib::Display,
}
unsafe impl Send for X11Emulation {}
impl X11Emulation {
pub(crate) fn new() -> Result<Self, X11EmulationCreationError> {
let display = unsafe {
match xlib::XOpenDisplay(ptr::null()) {
d if d == ptr::null::<xlib::Display>() as *mut xlib::Display => {
Err(X11EmulationCreationError::OpenDisplay)
}
display => Ok(display),
}
}?;
Ok(Self { display })
}
fn relative_motion(&self, dx: i32, dy: i32) {
unsafe {
xtest::XTestFakeRelativeMotionEvent(self.display, dx, dy, 0, 0);
}
}
fn emulate_mouse_button(&self, button: u32, state: u32) {
unsafe {
let x11_button = match button {
BTN_RIGHT => 3,
BTN_MIDDLE => 2,
BTN_BACK => 8,
BTN_FORWARD => 9,
BTN_LEFT => 1,
_ => 1,
};
xtest::XTestFakeButtonEvent(self.display, x11_button, state as i32, 0);
};
}
const SCROLL_UP: u32 = 4;
const SCROLL_DOWN: u32 = 5;
const SCROLL_LEFT: u32 = 6;
const SCROLL_RIGHT: u32 = 7;
fn emulate_scroll(&self, axis: u8, value: f64) {
let direction = match axis {
1 => {
if value < 0.0 {
Self::SCROLL_LEFT
} else {
Self::SCROLL_RIGHT
}
}
_ => {
if value < 0.0 {
Self::SCROLL_UP
} else {
Self::SCROLL_DOWN
}
}
};
unsafe {
xtest::XTestFakeButtonEvent(self.display, direction, 1, 0);
xtest::XTestFakeButtonEvent(self.display, direction, 0, 0);
}
}
#[allow(dead_code)]
fn emulate_key(&self, key: u32, state: u8) {
let key = key + 8; // xorg keycodes are shifted by 8
unsafe {
xtest::XTestFakeKeyEvent(self.display, key, state as i32, 0);
}
}
}
impl Drop for X11Emulation {
fn drop(&mut self) {
unsafe {
XCloseDisplay(self.display);
}
}
}
#[async_trait]
impl Emulation for X11Emulation {
async fn consume(&mut self, event: Event, _: EmulationHandle) -> Result<(), EmulationError> {
match event {
Event::Pointer(pointer_event) => match pointer_event {
PointerEvent::Motion { time: _, dx, dy } => {
self.relative_motion(dx as i32, dy as i32);
}
PointerEvent::Button {
time: _,
button,
state,
} => {
self.emulate_mouse_button(button, state);
}
PointerEvent::Axis {
time: _,
axis,
value,
} => {
self.emulate_scroll(axis, value);
}
PointerEvent::AxisDiscrete120 { axis, value } => {
self.emulate_scroll(axis, value as f64);
}
},
Event::Keyboard(KeyboardEvent::Key {
time: _,
key,
state,
}) => {
self.emulate_key(key, state);
}
_ => {}
}
unsafe {
xlib::XFlush(self.display);
}
// FIXME
Ok(())
}
async fn create(&mut self, _: EmulationHandle) {
// for our purposes it does not matter what client sent the event
}
async fn destroy(&mut self, _: EmulationHandle) {
// for our purposes it does not matter what client sent the event
}
async fn terminate(&mut self) {
/* nothing to do */
}
}

View File

@@ -0,0 +1,161 @@
use ashpd::{
desktop::{
remote_desktop::{Axis, DeviceType, KeyState, RemoteDesktop},
PersistMode, Session,
},
zbus::AsyncDrop,
};
use async_trait::async_trait;
use futures::FutureExt;
use input_event::{
Event::{Keyboard, Pointer},
KeyboardEvent, PointerEvent,
};
use crate::error::EmulationError;
use super::{error::XdpEmulationCreationError, Emulation, EmulationHandle};
pub(crate) struct DesktopPortalEmulation<'a> {
proxy: RemoteDesktop<'a>,
session: Session<'a, RemoteDesktop<'a>>,
}
impl<'a> DesktopPortalEmulation<'a> {
pub(crate) async fn new() -> Result<DesktopPortalEmulation<'a>, XdpEmulationCreationError> {
log::debug!("connecting to org.freedesktop.portal.RemoteDesktop portal ...");
let proxy = RemoteDesktop::new().await?;
// retry when user presses the cancel button
log::debug!("creating session ...");
let session = proxy.create_session().await?;
log::debug!("selecting devices ...");
proxy
.select_devices(
&session,
DeviceType::Keyboard | DeviceType::Pointer,
None,
PersistMode::ExplicitlyRevoked,
)
.await?;
log::info!("requesting permission for input emulation");
let _devices = proxy.start(&session, None).await?.response()?;
log::debug!("started session");
let session = session;
Ok(Self { proxy, session })
}
}
#[async_trait]
impl<'a> Emulation for DesktopPortalEmulation<'a> {
async fn consume(
&mut self,
event: input_event::Event,
_client: EmulationHandle,
) -> Result<(), EmulationError> {
match event {
Pointer(p) => match p {
PointerEvent::Motion { time: _, dx, dy } => {
self.proxy
.notify_pointer_motion(&self.session, dx, dy)
.await?;
}
PointerEvent::Button {
time: _,
button,
state,
} => {
let state = match state {
0 => KeyState::Released,
_ => KeyState::Pressed,
};
self.proxy
.notify_pointer_button(&self.session, button as i32, state)
.await?;
}
PointerEvent::AxisDiscrete120 { axis, value } => {
let axis = match axis {
0 => Axis::Vertical,
_ => Axis::Horizontal,
};
self.proxy
.notify_pointer_axis_discrete(&self.session, axis, value / 120)
.await?;
}
PointerEvent::Axis {
time: _,
axis,
value,
} => {
let axis = match axis {
0 => Axis::Vertical,
_ => Axis::Horizontal,
};
let (dx, dy) = match axis {
Axis::Vertical => (0., value),
Axis::Horizontal => (value, 0.),
};
self.proxy
.notify_pointer_axis(&self.session, dx, dy, true)
.await?;
}
},
Keyboard(k) => {
match k {
KeyboardEvent::Key {
time: _,
key,
state,
} => {
let state = match state {
0 => KeyState::Released,
_ => KeyState::Pressed,
};
self.proxy
.notify_keyboard_keycode(&self.session, key as i32, state)
.await?;
}
KeyboardEvent::Modifiers { .. } => {
// ignore
}
}
}
}
Ok(())
}
async fn create(&mut self, _client: EmulationHandle) {}
async fn destroy(&mut self, _client: EmulationHandle) {}
async fn terminate(&mut self) {
if let Err(e) = self.session.close().await {
log::warn!("session.close(): {e}");
};
if let Err(e) = self.session.receive_closed().await {
log::warn!("session.receive_closed(): {e}");
};
}
}
impl<'a> AsyncDrop for DesktopPortalEmulation<'a> {
#[doc = r" Perform the async cleanup."]
#[must_use]
#[allow(clippy::type_complexity, clippy::type_repetition_in_bounds)]
fn async_drop<'async_trait>(
self,
) -> ::core::pin::Pin<
Box<dyn ::core::future::Future<Output = ()> + ::core::marker::Send + 'async_trait>,
>
where
Self: 'async_trait,
{
async move {
let _ = self.session.close().await;
}
.boxed()
}
}

21
input-event/Cargo.toml Normal file
View File

@@ -0,0 +1,21 @@
[package]
name = "input-event"
description = "cross-platform input-event types for input-capture / input-emulation"
version = "0.3.0"
edition = "2021"
license = "GPL-3.0-or-later"
repository = "https://github.com/feschber/lan-mouse"
[dependencies]
futures-core = "0.3.30"
log = "0.4.22"
num_enum = "0.7.2"
serde = { version = "1.0", features = ["derive"] }
thiserror = "2.0.0"
[target.'cfg(all(unix, not(target_os="macos")))'.dependencies]
reis = { version = "0.4", optional = true }
[features]
default = ["libei"]
libei = ["dep:reis"]

1
input-event/src/error.rs Normal file
View File

@@ -0,0 +1 @@

119
input-event/src/lib.rs Normal file
View File

@@ -0,0 +1,119 @@
use std::fmt::{self, Display};
pub mod error;
pub mod scancode;
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
mod libei;
// FIXME
pub const BTN_LEFT: u32 = 0x110;
pub const BTN_RIGHT: u32 = 0x111;
pub const BTN_MIDDLE: u32 = 0x112;
pub const BTN_BACK: u32 = 0x113;
pub const BTN_FORWARD: u32 = 0x114;
#[derive(Debug, PartialEq, Clone, Copy)]
pub enum PointerEvent {
/// relative motion event
Motion { time: u32, dx: f64, dy: f64 },
/// mouse button event
Button { time: u32, button: u32, state: u32 },
/// axis event, scroll event for touchpads
Axis { time: u32, axis: u8, value: f64 },
/// discrete axis event, scroll event for mice - 120 = one scroll tick
AxisDiscrete120 { axis: u8, value: i32 },
}
#[derive(Debug, PartialEq, Clone, Copy)]
pub enum KeyboardEvent {
/// a key press / release event
Key { time: u32, key: u32, state: u8 },
/// modifiers changed state
Modifiers {
depressed: u32,
latched: u32,
locked: u32,
group: u32,
},
}
#[derive(PartialEq, Debug, Clone, Copy)]
pub enum Event {
/// pointer event (motion / button / axis)
Pointer(PointerEvent),
/// keyboard events (key / modifiers)
Keyboard(KeyboardEvent),
}
impl Display for PointerEvent {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
PointerEvent::Motion { time: _, dx, dy } => write!(f, "motion({dx},{dy})"),
PointerEvent::Button {
time: _,
button,
state,
} => {
let str = match *button {
BTN_LEFT => Some("left"),
BTN_RIGHT => Some("right"),
BTN_MIDDLE => Some("middle"),
BTN_FORWARD => Some("forward"),
BTN_BACK => Some("back"),
_ => None,
};
if let Some(button) = str {
write!(f, "button({button}, {state})")
} else {
write!(f, "button({button}, {state}")
}
}
PointerEvent::Axis {
time: _,
axis,
value,
} => write!(f, "scroll({axis}, {value})"),
PointerEvent::AxisDiscrete120 { axis, value } => {
write!(f, "scroll-120 ({axis}, {value})")
}
}
}
}
impl Display for KeyboardEvent {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
KeyboardEvent::Key {
time: _,
key,
state,
} => {
let scan = scancode::Linux::try_from(*key);
if let Ok(scan) = scan {
write!(f, "key({scan:?}, {state})")
} else {
write!(f, "key({key}, {state})")
}
}
KeyboardEvent::Modifiers {
depressed: mods_depressed,
latched: mods_latched,
locked: 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),
}
}
}

146
input-event/src/libei.rs Normal file
View File

@@ -0,0 +1,146 @@
use reis::{
ei::{button::ButtonState, keyboard::KeyState},
event::EiEvent,
};
use crate::{Event, KeyboardEvent, PointerEvent};
impl Event {
pub fn from_ei_event(ei_event: EiEvent) -> impl Iterator<Item = Self> {
to_input_events(ei_event).into_iter()
}
}
enum Events {
None,
One(Event),
Two(Event, Event),
}
impl Events {
fn into_iter(self) -> impl Iterator<Item = Event> {
EventIterator::new(self)
}
}
struct EventIterator {
events: [Option<Event>; 2],
pos: usize,
}
impl EventIterator {
fn new(events: Events) -> Self {
let events = match events {
Events::None => [None, None],
Events::One(e) => [Some(e), None],
Events::Two(e, f) => [Some(e), Some(f)],
};
Self { events, pos: 0 }
}
}
impl Iterator for EventIterator {
type Item = Event;
fn next(&mut self) -> Option<Self::Item> {
let res = if self.pos >= self.events.len() {
None
} else {
self.events[self.pos]
};
self.pos += 1;
res
}
}
fn to_input_events(ei_event: EiEvent) -> Events {
match ei_event {
EiEvent::KeyboardModifiers(mods) => {
let modifier_event = KeyboardEvent::Modifiers {
depressed: mods.depressed,
latched: mods.latched,
locked: mods.locked,
group: mods.group,
};
Events::One(Event::Keyboard(modifier_event))
}
EiEvent::Frame(_) => Events::None, /* FIXME */
EiEvent::PointerMotion(motion) => {
let motion_event = PointerEvent::Motion {
time: motion.time as u32,
dx: motion.dx as f64,
dy: motion.dy as f64,
};
Events::One(Event::Pointer(motion_event))
}
EiEvent::PointerMotionAbsolute(_) => Events::None,
EiEvent::Button(button) => {
let button_event = PointerEvent::Button {
time: button.time as u32,
button: button.button,
state: match button.state {
ButtonState::Released => 0,
ButtonState::Press => 1,
},
};
Events::One(Event::Pointer(button_event))
}
EiEvent::ScrollDelta(delta) => {
let dy = Event::Pointer(PointerEvent::Axis {
time: 0,
axis: 0,
value: delta.dy as f64,
});
let dx = Event::Pointer(PointerEvent::Axis {
time: 0,
axis: 1,
value: delta.dx as f64,
});
if delta.dy != 0. && delta.dx != 0. {
Events::Two(dy, dx)
} else if delta.dy != 0. {
Events::One(dy)
} else if delta.dx != 0. {
Events::One(dx)
} else {
Events::None
}
}
EiEvent::ScrollStop(_) => Events::None, /* TODO */
EiEvent::ScrollCancel(_) => Events::None, /* TODO */
EiEvent::ScrollDiscrete(scroll) => {
let dy = Event::Pointer(PointerEvent::AxisDiscrete120 {
axis: 0,
value: scroll.discrete_dy,
});
let dx = Event::Pointer(PointerEvent::AxisDiscrete120 {
axis: 1,
value: scroll.discrete_dx,
});
if scroll.discrete_dy != 0 && scroll.discrete_dx != 0 {
Events::Two(dy, dx)
} else if scroll.discrete_dy != 0 {
Events::One(dy)
} else if scroll.discrete_dx != 0 {
Events::One(dx)
} else {
Events::None
}
}
EiEvent::KeyboardKey(key) => {
let key_event = KeyboardEvent::Key {
key: key.key,
state: match key.state {
KeyState::Press => 1,
KeyState::Released => 0,
},
time: key.time as u32,
};
Events::One(Event::Keyboard(key_event))
}
EiEvent::TouchDown(_) => Events::None, /* TODO */
EiEvent::TouchUp(_) => Events::None, /* TODO */
EiEvent::TouchMotion(_) => Events::None, /* TODO */
_ => Events::None,
}
}

851
input-event/src/scancode.rs Normal file
View File

@@ -0,0 +1,851 @@
use num_enum::TryFromPrimitive;
use serde::{Deserialize, Serialize};
/*
* https://learn.microsoft.com/en-us/windows/win32/inputdev/about-keyboard-input
* https://download.microsoft.com/download/1/6/1/161ba512-40e2-4cc9-843a-923143f3456c/translate.pdf
* https://kbd-project.org/docs/scancodes/scancodes-1.html
*/
#[repr(u32)]
#[derive(Debug, Clone, Copy, TryFromPrimitive)]
pub enum Windows {
Shutdown = 0xE05E,
SystemSleep = 0xE05F,
SystemWakeUp = 0xE063,
ErrorRollOver = 0x00FF,
KeyA = 0x001E,
KeyB = 0x0030,
KeyC = 0x002E,
KeyD = 0x0020,
KeyE = 0x0012,
KeyF = 0x0021,
KeyG = 0x0022,
KeyH = 0x0023,
KeyI = 0x0017,
KeyJ = 0x0024,
KeyK = 0x0025,
KeyL = 0x0026,
KeyM = 0x0032,
KeyN = 0x0031,
KeyO = 0x0018,
KeyP = 0x0019,
KeyQ = 0x0010,
KeyR = 0x0013,
KeyS = 0x001F,
KeyT = 0x0014,
KeyU = 0x0016,
KeyV = 0x002F,
KeyW = 0x0011,
KeyX = 0x002D,
KeyY = 0x0015,
KeyZ = 0x002C,
Key1 = 0x0002,
Key2 = 0x0003,
Key3 = 0x0004,
Key4 = 0x0005,
Key5 = 0x0006,
Key6 = 0x0007,
Key7 = 0x0008,
Key8 = 0x0009,
Key9 = 0x000A,
Key0 = 0x000B,
KeyEnter = 0x001C,
KeyEsc = 0x0001,
KeyDelete = 0x000E,
KeyTab = 0x000F,
KeySpace = 0x0039,
KeyMinus = 0x000C,
KeyEqual = 0x000D,
KeyLeftBrace = 0x001A,
KeyRightBrace = 0x001B,
KeyBackslash = 0x002B,
KeySemiColon = 0x0027,
KeyApostrophe = 0x0028,
KeyGrave = 0x0029,
KeyComma = 0x0033,
KeyDot = 0x0034,
KeySlash = 0x0035,
KeyCapsLock = 0x003A,
KeyF1 = 0x003B,
KeyF2 = 0x003C,
KeyF3 = 0x003D,
KeyF4 = 0x003E,
KeyF5 = 0x003F,
KeyF6 = 0x0040,
KeyF7 = 0x0041,
KeyF8 = 0x0042,
KeyF9 = 0x0043,
KeyF10 = 0x0044,
KeyF11 = 0x0057,
KeyF12 = 0x0058,
KeyPrintScreen = 0xE037,
KeyScrollLock = 0x0046,
KeyPause = 0xE11D45,
KeyInsert = 0xE052,
KeyHome = 0xE047,
KeyPageUp = 0xE049,
KeyDeleteForward = 0xE053,
KeyEnd = 0xE04F,
KeyPageDown = 0xE051,
KeyRight = 0xE04D,
KeyLeft = 0xE04B,
KeyDown = 0xE050,
KeyUp = 0xE048,
KeypadNumLock = 0x0045,
KeypadSlash = 0xE035,
KeypadStar = 0x0037,
KeypadDash = 0x004A,
KeypadPlus = 0x004E,
KeypadEnter = 0xE01C,
Keypad1End = 0x004F,
Keypad2DownArrow = 0x0050,
Keypad3PageDn = 0x0051,
Keypad4LeftArrow = 0x004B,
Keypad5 = 0x004C,
Keypad6RightArrow = 0x004D,
Keypad7Home = 0x0047,
Keypad8UpArrow = 0x0048,
Keypad9PageUp = 0x0049,
Keypad0Insert = 0x0052,
KeypadDot = 0x0053,
KeyNonUSSlashBar = 0x0056,
KeyApplication = 0xE05D,
KeypadEquals = 0x0059,
KeyF13 = 0x0064,
KeyF14 = 0x0065,
KeyF15 = 0x0066,
KeyF16 = 0x0067,
KeyF17 = 0x0068,
KeyF18 = 0x0069,
KeyF19 = 0x006A,
KeyF20 = 0x006B,
KeyF21 = 0x006C,
KeyF22 = 0x006D,
KeyF23 = 0x006E,
KeyF24 = 0x0076, // KeyLANG5
KeypadComma = 0x007E,
KeyInternational1 = 0x0073,
KeyInternational2 = 0x0070,
KeyInternational3 = 0x007D, // typo in doc -> its Int'l 3 not Int'l 2
#[allow(dead_code)]
KeyInternational4 = 0x0079,
#[allow(dead_code)]
KeyInternational5 = 0x007B,
// KeyInternational6 = 0x005C,
KeyLANG1 = 0x0072,
KeyLANG2 = 0x0071,
KeyLANG3 = 0x0078,
KeyLANG4 = 0x0077,
// KeyLANG5 = 0x0076,
KeyLeftCtrl = 0x001D,
KeyLeftShift = 0x002A,
KeyLeftAlt = 0x0038,
KeyLeftGUI = 0xE05B,
KeyRightCtrl = 0xE01D,
KeyRightShift = 0x0036,
KeyFakeRightShift = 0xE036,
KeyRightAlt = 0xE038,
KeyRightGUI = 0xE05C,
KeyScanNextTrack = 0xE019,
KeyScanPreviousTrack = 0xE010,
KeyStop = 0xE024,
KeyPlayPause = 0xE022,
KeyMute = 0xE020,
KeyVolumeUp = 0xE030,
KeyVolumeDown = 0xE02E,
#[allow(dead_code)]
ALConsumerControlConfiguration = 0xE06D, // TODO Unused
ALEmailReader = 0xE06C,
ALCalculator = 0xE021,
ALLocalMachineBrowser = 0xE06B,
ACSearch = 0xE065,
ACHome = 0xE032,
ACBack = 0xE06A,
ACForward = 0xE069,
ACStop = 0xE068,
ACRefresh = 0xE067,
ACBookmarks = 0xE066,
}
/*
* https://github.com/torvalds/linux/blob/master/include/uapi/linux/input-event-codes.h
*/
#[repr(u32)]
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Eq, Hash, PartialEq, TryFromPrimitive)]
#[allow(dead_code)]
pub enum Linux {
KeyReserved = 0,
KeyEsc = 1,
Key1 = 2,
Key2 = 3,
Key3 = 4,
Key4 = 5,
Key5 = 6,
Key6 = 7,
Key7 = 8,
Key8 = 9,
Key9 = 10,
Key0 = 11,
KeyMinus = 12,
KeyEqual = 13,
KeyBackspace = 14,
KeyTab = 15,
KeyQ = 16,
KeyW = 17,
KeyE = 18,
KeyR = 19,
KeyT = 20,
KeyY = 21,
KeyU = 22,
KeyI = 23,
KeyO = 24,
KeyP = 25,
KeyLeftbrace = 26,
KeyRightbrace = 27,
KeyEnter = 28,
KeyLeftCtrl = 29,
KeyA = 30,
KeyS = 31,
KeyD = 32,
KeyF = 33,
KeyG = 34,
KeyH = 35,
KeyJ = 36,
KeyK = 37,
KeyL = 38,
KeySemicolon = 39,
KeyApostrophe = 40,
KeyGrave = 41,
KeyLeftShift = 42,
KeyBackslash = 43,
KeyZ = 44,
KeyX = 45,
KeyC = 46,
KeyV = 47,
KeyB = 48,
KeyN = 49,
KeyM = 50,
KeyComma = 51,
KeyDot = 52,
KeySlash = 53,
KeyRightShift = 54,
KeyKpAsterisk = 55,
KeyLeftAlt = 56,
KeySpace = 57,
KeyCapsLock = 58,
KeyF1 = 59,
KeyF2 = 60,
KeyF3 = 61,
KeyF4 = 62,
KeyF5 = 63,
KeyF6 = 64,
KeyF7 = 65,
KeyF8 = 66,
KeyF9 = 67,
KeyF10 = 68,
KeyNumlock = 69,
KeyScrollLock = 70,
KeyKp7 = 71,
KeyKp8 = 72,
KeyKp9 = 73,
KeyKpMinus = 74,
KeyKp4 = 75,
KeyKp5 = 76,
KeyKp6 = 77,
KeyKpplus = 78,
KeyKp1 = 79,
KeyKp2 = 80,
KeyKp3 = 81,
KeyKp0 = 82,
KeyKpDot = 83,
Invalid = 84,
KeyZenkakuhankaku = 85,
Key102nd = 86,
KeyF11 = 87,
KeyF12 = 88,
KeyRo = 89,
KeyKatakana = 90,
KeyHiragana = 91,
KeyHenkan = 92,
KeyKatakanahiragana = 93,
KeyMuhenkan = 94,
KeyKpJpComma = 95,
KeyKpEnter = 96,
KeyRightCtrl = 97,
KeyKpslash = 98,
KeySysrq = 99,
KeyRightalt = 100,
KeyLinefeed = 101,
KeyHome = 102,
KeyUp = 103,
KeyPageup = 104,
KeyLeft = 105,
KeyRight = 106,
KeyEnd = 107,
KeyDown = 108,
KeyPagedown = 109,
KeyInsert = 110,
KeyDelete = 111,
KeyMacro = 112,
KeyMute = 113,
KeyVolumeDown = 114,
KeyVolumeUp = 115,
KeyPower = 116, /* SC System Power Down */
KeyKpequal = 117,
KeyKpplusminus = 118,
KeyPause = 119,
KeyScale = 120, /* AL Compiz Scale (Expose) */
KeyKpcomma = 121,
KeyHanguel = 122,
// KEY_HANGUEL = KeyHangeul,
KeyHanja = 123,
KeyYen = 124,
KeyLeftMeta = 125,
KeyRightmeta = 126,
KeyCompose = 127,
KeyStop = 128, /* AC Stop */
KeyAgain = 129,
KeyProps = 130, /* AC Properties */
KeyUndo = 131, /* AC Undo */
KeyFront = 132,
KeyCopy = 133, /* AC Copy */
KeyOpen = 134, /* AC Open */
KeyPaste = 135, /* AC Paste */
KeyFind = 136, /* AC Search */
KeyCut = 137, /* AC Cut */
KeyHelp = 138, /* AL Integrated Help Center */
KeyMenu = 139, /* Menu (show menu) */
KeyCalc = 140, /* AL Calculator */
KeySetup = 141,
KeySleep = 142, /* SC System Sleep */
KeyWakeup = 143, /* System Wake Up */
KeyFile = 144, /* AL Local Machine Browser */
KeySendfile = 145,
KeyDeletefile = 146,
KeyXfer = 147,
KeyProg1 = 148,
KeyProg2 = 149,
KeyWww = 150, /* AL Internet Browser */
KeyMsdos = 151,
KeyCoffee = 152, /* AL Terminal Lock/Screensaver */
// KEY_SCREENLOCK = KeyCoffee,
KeyRotateDisplay = 153, /* Display orientation for e.g. tablets */
// KEY_DIRECTION = KeyRotateDisplay,
KeyCyclewindows = 154,
KeyMail = 155,
KeyBookmarks = 156, /* AC Bookmarks */
KeyComputer = 157,
KeyBack = 158, /* AC Back */
KeyForward = 159, /* AC Forward */
KeyClosecd = 160,
KeyEjectcd = 161,
KeyEjectclosecd = 162,
KeyNextsong = 163,
KeyPlaypause = 164,
KeyPrevioussong = 165,
KeyStopcd = 166,
KeyRecord = 167,
KeyRewind = 168,
KeyPhone = 169, /* Media Select Telephone */
KeyIso = 170,
KeyConfig = 171, /* AL Consumer Control Configuration */
KeyHomepage = 172, /* AC Home */
KeyRefresh = 173, /* AC Refresh */
KeyExit = 174, /* AC Exit */
KeyMove = 175,
KeyEdit = 176,
KeyScrollup = 177,
KeyScrolldown = 178,
KeyKpleftparen = 179,
KeyKprightparen = 180,
KeyNew = 181, /* AC New */
KeyRedo = 182, /* AC Redo/Repeat */
KeyF13 = 183,
KeyF14 = 184,
KeyF15 = 185,
KeyF16 = 186,
KeyF17 = 187,
KeyF18 = 188,
KeyF19 = 189,
KeyF20 = 190,
KeyF21 = 191,
KeyF22 = 192,
KeyF23 = 193,
KeyF24 = 194,
Invalid1 = 195,
Invalid2 = 196,
Invalid3 = 197,
Invalid4 = 198,
Invalid5 = 199,
KeyPlaycd = 200,
KeyPausecd = 201,
KeyProg3 = 202,
KeyProg4 = 203,
KeyAllApplications = 204, /* AC Desktop Show All Applications */
// KEY_DASHBOARD = KeyAllApplications,
KeySuspend = 205,
KeyClose = 206, /* AC Close */
KeyPlay = 207,
KeyFastforward = 208,
KeyBassboost = 209,
KeyPrint = 210, /* AC Print */
KeyHp = 211,
KeyCamera = 212,
KeySound = 213,
KeyQuestion = 214,
KeyEmail = 215,
KeyChat = 216,
KeySearch = 217,
KeyConnect = 218,
KeyFinance = 219, /* AL Checkbook/Finance */
KeySport = 220,
KeyShop = 221,
KeyAlterase = 222,
KeyCancel = 223, /* AC Cancel */
KeyBrightnessdown = 224,
KeyBrightnessup = 225,
KeyMedia = 226,
KeySwitchvideomode = 227, /* Cycle between available video, outputs (Monitor/LCD/TV-out/etc) */
KeyKbdillumtoggle = 228,
KeyKbdillumdown = 229,
KeyKbdillumup = 230,
KeySend = 231, /* AC Send */
KeyReply = 232, /* AC Reply */
KeyForwardmail = 233, /* AC Forward Msg */
KeySave = 234, /* AC Save */
KeyDocuments = 235,
KeyBattery = 236,
KeyBluetooth = 237,
KeyWlan = 238,
KeyUwb = 239,
KeyUnknown = 240,
KeyVideoNext = 241, /* drive next video source */
KeyVideoPrev = 242, /* drive previous video source */
KeyBrightnessCycle = 243, /* brightness up, after max is min */
KeyBrightnessAuto = 244, /* Set Auto Brightness: manual, brightness control is off, rely on ambient */
// KEY_BRIGHTNESS_ZERO=KeyBrightnessAuto,
KeyDisplayOff = 245, /* display device to off state */
KeyWwan = 246, /* Wireless WAN (LTE, UMTS, GSM, etc.) */
// KEY_WIMAX = KeyWwan,
KeyRfkill = 247, /* Key that controls all radios */
KeyMicmute = 248, /* Mute / unmute the microphone */
KeyCount = 249,
}
impl TryFrom<Linux> for Windows {
type Error = ();
fn try_from(value: Linux) -> Result<Self, Self::Error> {
match value {
Linux::KeyReserved => Err(()),
Linux::KeyEsc => Ok(Self::KeyEsc),
Linux::Key1 => Ok(Self::Key1),
Linux::Key2 => Ok(Self::Key2),
Linux::Key3 => Ok(Self::Key3),
Linux::Key4 => Ok(Self::Key4),
Linux::Key5 => Ok(Self::Key5),
Linux::Key6 => Ok(Self::Key6),
Linux::Key7 => Ok(Self::Key7),
Linux::Key8 => Ok(Self::Key8),
Linux::Key9 => Ok(Self::Key9),
Linux::Key0 => Ok(Self::Key0),
Linux::KeyMinus => Ok(Self::KeyMinus),
Linux::KeyEqual => Ok(Self::KeyEqual),
Linux::KeyBackspace => Ok(Self::KeyDelete),
Linux::KeyTab => Ok(Self::KeyTab),
Linux::KeyQ => Ok(Self::KeyQ),
Linux::KeyW => Ok(Self::KeyW),
Linux::KeyE => Ok(Self::KeyE),
Linux::KeyR => Ok(Self::KeyR),
Linux::KeyT => Ok(Self::KeyT),
Linux::KeyY => Ok(Self::KeyY),
Linux::KeyU => Ok(Self::KeyU),
Linux::KeyI => Ok(Self::KeyI),
Linux::KeyO => Ok(Self::KeyO),
Linux::KeyP => Ok(Self::KeyP),
Linux::KeyLeftbrace => Ok(Self::KeyLeftBrace),
Linux::KeyRightbrace => Ok(Self::KeyRightBrace),
Linux::KeyEnter => Ok(Self::KeyEnter),
Linux::KeyLeftCtrl => Ok(Self::KeyLeftCtrl),
Linux::KeyA => Ok(Self::KeyA),
Linux::KeyS => Ok(Self::KeyS),
Linux::KeyD => Ok(Self::KeyD),
Linux::KeyF => Ok(Self::KeyF),
Linux::KeyG => Ok(Self::KeyG),
Linux::KeyH => Ok(Self::KeyH),
Linux::KeyJ => Ok(Self::KeyJ),
Linux::KeyK => Ok(Self::KeyK),
Linux::KeyL => Ok(Self::KeyL),
Linux::KeySemicolon => Ok(Self::KeySemiColon),
Linux::KeyApostrophe => Ok(Self::KeyApostrophe),
Linux::KeyGrave => Ok(Self::KeyGrave),
Linux::KeyLeftShift => Ok(Self::KeyLeftShift),
Linux::KeyBackslash => Ok(Self::KeyBackslash),
Linux::KeyZ => Ok(Self::KeyZ),
Linux::KeyX => Ok(Self::KeyX),
Linux::KeyC => Ok(Self::KeyC),
Linux::KeyV => Ok(Self::KeyV),
Linux::KeyB => Ok(Self::KeyB),
Linux::KeyN => Ok(Self::KeyN),
Linux::KeyM => Ok(Self::KeyM),
Linux::KeyComma => Ok(Self::KeyComma),
Linux::KeyDot => Ok(Self::KeyDot),
Linux::KeySlash => Ok(Self::KeySlash),
Linux::KeyRightShift => Ok(Self::KeyRightShift),
Linux::KeyKpAsterisk => Ok(Self::KeypadStar),
Linux::KeyLeftAlt => Ok(Self::KeyLeftAlt),
Linux::KeySpace => Ok(Self::KeySpace),
Linux::KeyCapsLock => Ok(Self::KeyCapsLock),
Linux::KeyF1 => Ok(Self::KeyF1),
Linux::KeyF2 => Ok(Self::KeyF2),
Linux::KeyF3 => Ok(Self::KeyF3),
Linux::KeyF4 => Ok(Self::KeyF4),
Linux::KeyF5 => Ok(Self::KeyF5),
Linux::KeyF6 => Ok(Self::KeyF6),
Linux::KeyF7 => Ok(Self::KeyF7),
Linux::KeyF8 => Ok(Self::KeyF8),
Linux::KeyF9 => Ok(Self::KeyF9),
Linux::KeyF10 => Ok(Self::KeyF10),
Linux::KeyNumlock => Ok(Self::KeypadNumLock),
Linux::KeyScrollLock => Ok(Self::KeyScrollLock),
Linux::KeyKp7 => Ok(Self::Keypad7Home),
Linux::KeyKp8 => Ok(Self::Keypad8UpArrow),
Linux::KeyKp9 => Ok(Self::Keypad9PageUp),
Linux::KeyKpMinus => Ok(Self::KeypadDash),
Linux::KeyKp4 => Ok(Self::Keypad4LeftArrow),
Linux::KeyKp5 => Ok(Self::Keypad5),
Linux::KeyKp6 => Ok(Self::Keypad6RightArrow),
Linux::KeyKpplus => Ok(Self::KeypadPlus),
Linux::KeyKp1 => Ok(Self::Keypad1End),
Linux::KeyKp2 => Ok(Self::Keypad2DownArrow),
Linux::KeyKp3 => Ok(Self::Keypad3PageDn),
Linux::KeyKp0 => Ok(Self::Keypad0Insert),
Linux::KeyKpDot => Ok(Self::KeypadDot),
Linux::KeyZenkakuhankaku => Ok(Self::KeyF24), // KeyLANG5
Linux::Key102nd => Ok(Self::KeyNonUSSlashBar), // TODO unsure
Linux::KeyF11 => Ok(Self::KeyF11),
Linux::KeyF12 => Ok(Self::KeyF12),
Linux::KeyRo => Ok(Self::KeyInternational1),
Linux::KeyKatakana => Ok(Self::KeyLANG3),
Linux::KeyHiragana => Ok(Self::KeyLANG4),
Linux::KeyHenkan => Ok(Self::KeyInternational4),
Linux::KeyKatakanahiragana => Ok(Self::KeyInternational2),
Linux::KeyMuhenkan => Ok(Self::KeyInternational5),
Linux::KeyKpJpComma => Ok(Self::KeypadComma),
Linux::KeyKpEnter => Ok(Self::KeypadEnter),
Linux::KeyRightCtrl => Ok(Self::KeyRightCtrl),
Linux::KeyKpslash => Ok(Self::KeypadSlash),
Linux::KeySysrq => Ok(Self::KeyPrintScreen), // TODO Windows does not have Sysrq, right?
Linux::KeyRightalt => Ok(Self::KeyRightAlt),
Linux::KeyLinefeed => Ok(Self::KeyEnter), // TODO unsure
Linux::KeyHome => Ok(Self::KeyHome),
Linux::KeyUp => Ok(Self::KeyUp),
Linux::KeyPageup => Ok(Self::KeyPageUp),
Linux::KeyLeft => Ok(Self::KeyLeft),
Linux::KeyRight => Ok(Self::KeyRight),
Linux::KeyEnd => Ok(Self::KeyEnd),
Linux::KeyDown => Ok(Self::KeyDown),
Linux::KeyPagedown => Ok(Self::KeyPageDown),
Linux::KeyInsert => Ok(Self::KeyInsert),
Linux::KeyDelete => Ok(Self::KeyDeleteForward),
Linux::KeyMacro => Err(()), // TODO
Linux::KeyMute => Ok(Self::KeyMute),
Linux::KeyVolumeDown => Ok(Self::KeyVolumeDown),
Linux::KeyVolumeUp => Ok(Self::KeyVolumeUp),
Linux::KeyPower => Ok(Self::Shutdown),
Linux::KeyKpequal => Ok(Self::KeypadEquals),
Linux::KeyKpplusminus => Ok(Self::KeypadPlus),
Linux::KeyPause => Ok(Self::KeyPause),
Linux::KeyScale => Err(()), // TODO
Linux::KeyKpcomma => Ok(Self::KeypadComma),
Linux::KeyHanguel => Ok(Self::KeyLANG1), // FIXME should be 00F2?
Linux::KeyHanja => Ok(Self::KeyLANG2), // FIXME should be 00F1?
Linux::KeyYen => Ok(Self::KeyInternational3),
Linux::KeyLeftMeta => Ok(Self::KeyLeftGUI),
Linux::KeyRightmeta => Ok(Self::KeyRightGUI),
Linux::KeyCompose => Ok(Self::KeyApplication),
Linux::KeyStop => Ok(Self::ACStop),
Linux::KeyAgain => Err(()),
Linux::KeyProps => Err(()),
Linux::KeyUndo => Err(()),
Linux::KeyFront => Err(()),
Linux::KeyCopy => Err(()),
Linux::KeyOpen => Err(()),
Linux::KeyPaste => Err(()),
Linux::KeyFind => Ok(Self::ACSearch),
Linux::KeyCut => Err(()),
Linux::KeyHelp => Ok(Self::KeyF1), // AL Integrated Help Center?
Linux::KeyMenu => Ok(Self::KeyApplication),
Linux::KeyCalc => Ok(Self::ALCalculator),
Linux::KeySetup => Err(()),
Linux::KeySleep => Ok(Self::SystemSleep),
Linux::KeyWakeup => Ok(Self::SystemWakeUp),
Linux::KeyFile => Ok(Self::ALLocalMachineBrowser),
Linux::KeySendfile => Err(()),
Linux::KeyDeletefile => Err(()),
Linux::KeyXfer => Err(()),
Linux::KeyProg1 => Err(()),
Linux::KeyProg2 => Err(()),
Linux::KeyWww => Ok(Self::ACSearch), // TODO unsure
Linux::KeyMsdos => Err(()),
Linux::KeyCoffee => Err(()),
Linux::KeyRotateDisplay => Err(()),
Linux::KeyCyclewindows => Err(()),
Linux::KeyMail => Ok(Self::ALEmailReader),
Linux::KeyBookmarks => Ok(Self::ACBookmarks),
Linux::KeyComputer => Ok(Self::ACHome),
Linux::KeyBack => Ok(Self::ACBack),
Linux::KeyForward => Ok(Self::ACForward),
Linux::KeyClosecd => Err(()),
Linux::KeyEjectcd => Err(()),
Linux::KeyEjectclosecd => Err(()),
Linux::KeyNextsong => Ok(Self::KeyScanNextTrack),
Linux::KeyPlaypause => Ok(Self::KeyPlayPause),
Linux::KeyPrevioussong => Ok(Self::KeyScanPreviousTrack),
Linux::KeyStopcd => Ok(Self::KeyStop),
Linux::KeyRecord => Err(()),
Linux::KeyRewind => Err(()),
Linux::KeyPhone => Err(()),
Linux::KeyIso => Err(()),
Linux::KeyConfig => Err(()),
Linux::KeyHomepage => Ok(Self::ACHome),
Linux::KeyRefresh => Ok(Self::ACRefresh),
Linux::KeyExit => Err(()),
Linux::KeyMove => Err(()),
Linux::KeyEdit => Err(()),
Linux::KeyScrollup => Err(()),
Linux::KeyScrolldown => Err(()),
Linux::KeyKpleftparen => Err(()),
Linux::KeyKprightparen => Err(()),
Linux::KeyNew => Err(()),
Linux::KeyRedo => Err(()),
Linux::KeyF13 => Ok(Self::KeyF13),
Linux::KeyF14 => Ok(Self::KeyF14),
Linux::KeyF15 => Ok(Self::KeyF15),
Linux::KeyF16 => Ok(Self::KeyF16),
Linux::KeyF17 => Ok(Self::KeyF17),
Linux::KeyF18 => Ok(Self::KeyF18),
Linux::KeyF19 => Ok(Self::KeyF19),
Linux::KeyF20 => Ok(Self::KeyF20),
Linux::KeyF21 => Ok(Self::KeyF21),
Linux::KeyF22 => Ok(Self::KeyF22),
Linux::KeyF23 => Ok(Self::KeyF23),
Linux::KeyF24 => Ok(Self::KeyF24),
Linux::KeyPlaycd => Err(()),
Linux::KeyPausecd => Err(()),
Linux::KeyProg3 => Err(()),
Linux::KeyProg4 => Err(()),
Linux::KeyAllApplications => Err(()),
Linux::KeySuspend => Err(()),
Linux::KeyClose => Err(()),
Linux::KeyPlay => Err(()),
Linux::KeyFastforward => Err(()),
Linux::KeyBassboost => Err(()),
Linux::KeyPrint => Err(()),
Linux::KeyHp => Err(()),
Linux::KeyCamera => Err(()),
Linux::KeySound => Err(()),
Linux::KeyQuestion => Err(()),
Linux::KeyEmail => Err(()),
Linux::KeyChat => Err(()),
Linux::KeySearch => Err(()),
Linux::KeyConnect => Err(()),
Linux::KeyFinance => Err(()),
Linux::KeySport => Err(()),
Linux::KeyShop => Err(()),
Linux::KeyAlterase => Err(()),
Linux::KeyCancel => Err(()),
Linux::KeyBrightnessdown => Err(()),
Linux::KeyBrightnessup => Err(()),
Linux::KeyMedia => Err(()),
Linux::KeySwitchvideomode => Err(()),
Linux::KeyKbdillumtoggle => Err(()),
Linux::KeyKbdillumdown => Err(()),
Linux::KeyKbdillumup => Err(()),
Linux::KeySend => Err(()),
Linux::KeyReply => Err(()),
Linux::KeyForwardmail => Err(()),
Linux::KeySave => Err(()),
Linux::KeyDocuments => Err(()),
Linux::KeyBattery => Err(()),
Linux::KeyBluetooth => Err(()),
Linux::KeyWlan => Err(()),
Linux::KeyUwb => Err(()),
Linux::KeyUnknown => Err(()),
Linux::KeyVideoNext => Err(()),
Linux::KeyVideoPrev => Err(()),
Linux::KeyBrightnessCycle => Err(()),
Linux::KeyBrightnessAuto => Err(()),
Linux::KeyDisplayOff => Err(()),
Linux::KeyWwan => Err(()),
Linux::KeyRfkill => Err(()),
Linux::KeyMicmute => Err(()),
Linux::KeyCount => Err(()),
Linux::Invalid => Err(()),
Linux::Invalid1 => Err(()),
Linux::Invalid2 => Err(()),
Linux::Invalid3 => Err(()),
Linux::Invalid4 => Err(()),
Linux::Invalid5 => Err(()),
}
}
}
impl TryFrom<Windows> for Linux {
type Error = ();
fn try_from(value: Windows) -> Result<Self, Self::Error> {
match value {
Windows::Shutdown => Ok(Self::KeyPower),
Windows::SystemSleep => Ok(Self::KeySleep),
Windows::SystemWakeUp => Ok(Self::KeyWakeup),
Windows::ErrorRollOver => Ok(Self::KeyRo),
Windows::KeyA => Ok(Self::KeyA),
Windows::KeyB => Ok(Self::KeyB),
Windows::KeyC => Ok(Self::KeyC),
Windows::KeyD => Ok(Self::KeyD),
Windows::KeyE => Ok(Self::KeyE),
Windows::KeyF => Ok(Self::KeyF),
Windows::KeyG => Ok(Self::KeyG),
Windows::KeyH => Ok(Self::KeyH),
Windows::KeyI => Ok(Self::KeyI),
Windows::KeyJ => Ok(Self::KeyJ),
Windows::KeyK => Ok(Self::KeyK),
Windows::KeyL => Ok(Self::KeyL),
Windows::KeyM => Ok(Self::KeyM),
Windows::KeyN => Ok(Self::KeyN),
Windows::KeyO => Ok(Self::KeyO),
Windows::KeyP => Ok(Self::KeyP),
Windows::KeyQ => Ok(Self::KeyQ),
Windows::KeyR => Ok(Self::KeyR),
Windows::KeyS => Ok(Self::KeyS),
Windows::KeyT => Ok(Self::KeyT),
Windows::KeyU => Ok(Self::KeyU),
Windows::KeyV => Ok(Self::KeyV),
Windows::KeyW => Ok(Self::KeyW),
Windows::KeyX => Ok(Self::KeyX),
Windows::KeyY => Ok(Self::KeyY),
Windows::KeyZ => Ok(Self::KeyZ),
Windows::Key1 => Ok(Self::Key1),
Windows::Key2 => Ok(Self::Key2),
Windows::Key3 => Ok(Self::Key3),
Windows::Key4 => Ok(Self::Key4),
Windows::Key5 => Ok(Self::Key5),
Windows::Key6 => Ok(Self::Key6),
Windows::Key7 => Ok(Self::Key7),
Windows::Key8 => Ok(Self::Key8),
Windows::Key9 => Ok(Self::Key9),
Windows::Key0 => Ok(Self::Key0),
Windows::KeyEnter => Ok(Self::KeyEnter),
Windows::KeyEsc => Ok(Self::KeyEsc),
Windows::KeyDelete => Ok(Self::KeyBackspace),
Windows::KeyTab => Ok(Self::KeyTab),
Windows::KeySpace => Ok(Self::KeySpace),
Windows::KeyMinus => Ok(Self::KeyMinus),
Windows::KeyEqual => Ok(Self::KeyEqual),
Windows::KeyLeftBrace => Ok(Self::KeyLeftbrace),
Windows::KeyRightBrace => Ok(Self::KeyRightbrace),
Windows::KeyBackslash => Ok(Self::KeyBackslash),
Windows::KeySemiColon => Ok(Self::KeySemicolon),
Windows::KeyApostrophe => Ok(Self::KeyApostrophe),
Windows::KeyGrave => Ok(Self::KeyGrave),
Windows::KeyComma => Ok(Self::KeyComma),
Windows::KeyDot => Ok(Self::KeyDot),
Windows::KeySlash => Ok(Self::KeySlash),
Windows::KeyCapsLock => Ok(Self::KeyCapsLock),
Windows::KeyF1 => Ok(Self::KeyF1),
Windows::KeyF2 => Ok(Self::KeyF2),
Windows::KeyF3 => Ok(Self::KeyF3),
Windows::KeyF4 => Ok(Self::KeyF4),
Windows::KeyF5 => Ok(Self::KeyF5),
Windows::KeyF6 => Ok(Self::KeyF6),
Windows::KeyF7 => Ok(Self::KeyF7),
Windows::KeyF8 => Ok(Self::KeyF8),
Windows::KeyF9 => Ok(Self::KeyF9),
Windows::KeyF10 => Ok(Self::KeyF10),
Windows::KeyF11 => Ok(Self::KeyF11),
Windows::KeyF12 => Ok(Self::KeyF12),
Windows::KeyPrintScreen => Ok(Self::KeySysrq),
Windows::KeyScrollLock => Ok(Self::KeyScrollLock),
Windows::KeyPause => Ok(Self::KeyPause),
Windows::KeyInsert => Ok(Self::KeyInsert),
Windows::KeyHome => Ok(Self::KeyHome),
Windows::KeyPageUp => Ok(Self::KeyPageup),
Windows::KeyDeleteForward => Ok(Self::KeyDelete),
Windows::KeyEnd => Ok(Self::KeyEnd),
Windows::KeyPageDown => Ok(Self::KeyPagedown),
Windows::KeyRight => Ok(Self::KeyRight),
Windows::KeyLeft => Ok(Self::KeyLeft),
Windows::KeyDown => Ok(Self::KeyDown),
Windows::KeyUp => Ok(Self::KeyUp),
Windows::KeypadNumLock => Ok(Self::KeyNumlock),
Windows::KeypadSlash => Ok(Self::KeyKpslash),
Windows::KeypadStar => Ok(Self::KeyKpAsterisk),
Windows::KeypadDash => Ok(Self::KeyKpMinus),
Windows::KeypadPlus => Ok(Self::KeyKpplus),
Windows::KeypadEnter => Ok(Self::KeyKpEnter),
Windows::Keypad1End => Ok(Self::KeyKp1),
Windows::Keypad2DownArrow => Ok(Self::KeyKp2),
Windows::Keypad3PageDn => Ok(Self::KeyKp3),
Windows::Keypad4LeftArrow => Ok(Self::KeyKp4),
Windows::Keypad5 => Ok(Self::KeyKp5),
Windows::Keypad6RightArrow => Ok(Self::KeyKp6),
Windows::Keypad7Home => Ok(Self::KeyKp7),
Windows::Keypad8UpArrow => Ok(Self::KeyKp8),
Windows::Keypad9PageUp => Ok(Self::KeyKp9),
Windows::Keypad0Insert => Ok(Self::KeyKp0),
Windows::KeypadDot => Ok(Self::KeyKpDot),
Windows::KeyNonUSSlashBar => Ok(Self::Key102nd),
Windows::KeyApplication => Ok(Self::KeyMenu),
Windows::KeypadEquals => Ok(Self::KeyKpequal),
Windows::KeyF13 => Ok(Self::KeyF13),
Windows::KeyF14 => Ok(Self::KeyF14),
Windows::KeyF15 => Ok(Self::KeyF15),
Windows::KeyF16 => Ok(Self::KeyF16),
Windows::KeyF17 => Ok(Self::KeyF17),
Windows::KeyF18 => Ok(Self::KeyF18),
Windows::KeyF19 => Ok(Self::KeyF19),
Windows::KeyF20 => Ok(Self::KeyF20),
Windows::KeyF21 => Ok(Self::KeyF21),
Windows::KeyF22 => Ok(Self::KeyF22),
Windows::KeyF23 => Ok(Self::KeyF23),
Windows::KeyF24 => Ok(Self::KeyF24),
Windows::KeypadComma => Ok(Self::KeyKpcomma),
Windows::KeyInternational1 => Ok(Self::KeyRo),
Windows::KeyInternational2 => Ok(Self::KeyKatakanahiragana),
Windows::KeyInternational3 => Ok(Self::KeyYen),
Windows::KeyInternational4 => Ok(Self::KeyHenkan),
Windows::KeyInternational5 => Ok(Self::KeyMuhenkan),
Windows::KeyLANG1 => Ok(Self::KeyHanguel),
Windows::KeyLANG2 => Ok(Self::KeyHanja),
Windows::KeyLANG3 => Ok(Self::KeyKatakana),
Windows::KeyLANG4 => Ok(Self::KeyHiragana),
Windows::KeyLeftCtrl => Ok(Self::KeyLeftCtrl),
Windows::KeyLeftShift => Ok(Self::KeyLeftShift),
Windows::KeyLeftAlt => Ok(Self::KeyLeftAlt),
Windows::KeyLeftGUI => Ok(Self::KeyLeftMeta),
Windows::KeyRightCtrl => Ok(Self::KeyRightCtrl),
Windows::KeyRightShift => Ok(Self::KeyRightShift),
Windows::KeyFakeRightShift => Ok(Self::KeyRightShift),
Windows::KeyRightAlt => Ok(Self::KeyRightalt),
Windows::KeyRightGUI => Ok(Self::KeyRightmeta),
Windows::KeyScanNextTrack => Ok(Self::KeyNextsong),
Windows::KeyScanPreviousTrack => Ok(Self::KeyPrevioussong),
Windows::KeyStop => Ok(Self::KeyStopcd),
Windows::KeyPlayPause => Ok(Self::KeyPlaypause),
Windows::KeyMute => Ok(Self::KeyMute),
Windows::KeyVolumeUp => Ok(Self::KeyVolumeUp),
Windows::KeyVolumeDown => Ok(Self::KeyVolumeDown),
Windows::ALConsumerControlConfiguration => Err(()),
Windows::ALEmailReader => Ok(Self::KeyMail),
Windows::ALCalculator => Ok(Self::KeyCalc),
Windows::ALLocalMachineBrowser => Ok(Self::KeyFile),
Windows::ACSearch => Ok(Self::KeyWww),
Windows::ACHome => Ok(Self::KeyHomepage),
Windows::ACBack => Ok(Self::KeyBack),
Windows::ACForward => Ok(Self::KeyForward),
Windows::ACStop => Ok(Self::KeyStop),
Windows::ACRefresh => Ok(Self::KeyRefresh),
Windows::ACBookmarks => Ok(Self::KeyBookmarks),
}
}
}

18
lan-mouse-cli/Cargo.toml Normal file
View File

@@ -0,0 +1,18 @@
[package]
name = "lan-mouse-cli"
description = "CLI Frontend for lan-mouse"
version = "0.2.0"
edition = "2021"
license = "GPL-3.0-or-later"
repository = "https://github.com/feschber/lan-mouse"
[dependencies]
futures = "0.3.30"
lan-mouse-ipc = { path = "../lan-mouse-ipc", version = "0.2.0" }
tokio = { version = "1.32.0", features = [
"io-util",
"io-std",
"macros",
"net",
"rt",
] }

View File

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

309
lan-mouse-cli/src/lib.rs Normal file
View File

@@ -0,0 +1,309 @@
use futures::StreamExt;
use tokio::{
io::{AsyncBufReadExt, BufReader},
task::LocalSet,
};
use std::io::{self, Write};
use self::command::{Command, CommandType};
use lan_mouse_ipc::{
AsyncFrontendEventReader, AsyncFrontendRequestWriter, ClientConfig, ClientHandle, ClientState,
FrontendEvent, FrontendRequest, IpcError, DEFAULT_PORT,
};
mod command;
pub fn run() -> Result<(), IpcError> {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_io()
.enable_time()
.build()?;
runtime.block_on(LocalSet::new().run_until(async move {
let (rx, tx) = lan_mouse_ipc::connect_async().await?;
let mut cli = Cli::new(rx, tx);
cli.run().await
}))?;
Ok(())
}
struct Cli {
clients: Vec<(ClientHandle, ClientConfig, ClientState)>,
changed: Option<ClientHandle>,
rx: AsyncFrontendEventReader,
tx: AsyncFrontendRequestWriter,
}
impl Cli {
fn new(rx: AsyncFrontendEventReader, tx: AsyncFrontendRequestWriter) -> Cli {
Self {
clients: vec![],
changed: None,
rx,
tx,
}
}
async fn run(&mut self) -> Result<(), IpcError> {
let stdin = tokio::io::stdin();
let stdin = BufReader::new(stdin);
let mut stdin = stdin.lines();
/* initial state sync */
self.clients = loop {
match self.rx.next().await {
Some(Ok(e)) => {
if let FrontendEvent::Enumerate(clients) = e {
break clients;
}
}
Some(Err(e)) => return Err(e),
None => return Ok(()),
}
};
loop {
prompt()?;
tokio::select! {
line = stdin.next_line() => {
let Some(line) = line? else {
break Ok(());
};
let cmd: Command = match line.parse() {
Ok(cmd) => cmd,
Err(e) => {
eprintln!("{e}");
continue;
}
};
self.execute(cmd).await?;
}
event = self.rx.next() => {
if let Some(event) = event {
self.handle_event(event?);
} else {
break Ok(());
}
}
}
if let Some(handle) = self.changed.take() {
self.update_client(handle).await?;
}
}
}
async fn update_client(&mut self, handle: ClientHandle) -> Result<(), IpcError> {
self.tx.request(FrontendRequest::GetState(handle)).await?;
while let Some(Ok(event)) = self.rx.next().await {
self.handle_event(event.clone());
if let FrontendEvent::State(_, _, _) | FrontendEvent::NoSuchClient(_) = event {
break;
}
}
Ok(())
}
async fn execute(&mut self, cmd: Command) -> Result<(), IpcError> {
match cmd {
Command::None => {}
Command::Connect(pos, host, port) => {
let request = FrontendRequest::Create;
self.tx.request(request).await?;
let handle = loop {
if let Some(Ok(event)) = self.rx.next().await {
match event {
FrontendEvent::Created(h, c, s) => {
self.clients.push((h, c, s));
break h;
}
_ => {
self.handle_event(event);
continue;
}
}
}
};
for request in [
FrontendRequest::UpdateHostname(handle, Some(host.clone())),
FrontendRequest::UpdatePort(handle, port.unwrap_or(DEFAULT_PORT)),
FrontendRequest::UpdatePosition(handle, pos),
] {
self.tx.request(request).await?;
}
self.update_client(handle).await?;
}
Command::Disconnect(id) => {
self.tx.request(FrontendRequest::Delete(id)).await?;
loop {
if let Some(Ok(event)) = self.rx.next().await {
self.handle_event(event.clone());
if let FrontendEvent::Deleted(_) = event {
self.handle_event(event);
break;
}
}
}
}
Command::Activate(id) => {
self.tx.request(FrontendRequest::Activate(id, true)).await?;
self.update_client(id).await?;
}
Command::Deactivate(id) => {
self.tx
.request(FrontendRequest::Activate(id, false))
.await?;
self.update_client(id).await?;
}
Command::List => {
self.tx.request(FrontendRequest::Enumerate()).await?;
while let Some(e) = self.rx.next().await {
let event = e?;
self.handle_event(event.clone());
if let FrontendEvent::Enumerate(_) = event {
break;
}
}
}
Command::SetHost(handle, host) => {
let request = FrontendRequest::UpdateHostname(handle, Some(host.clone()));
self.tx.request(request).await?;
self.update_client(handle).await?;
}
Command::SetPort(handle, port) => {
let request = FrontendRequest::UpdatePort(handle, port.unwrap_or(DEFAULT_PORT));
self.tx.request(request).await?;
self.update_client(handle).await?;
}
Command::Help => {
for cmd_type in [
CommandType::List,
CommandType::Connect,
CommandType::Disconnect,
CommandType::Activate,
CommandType::Deactivate,
CommandType::SetHost,
CommandType::SetPort,
] {
eprintln!("{}", cmd_type.usage());
}
}
}
Ok(())
}
fn find_mut(
&mut self,
handle: ClientHandle,
) -> Option<&mut (ClientHandle, ClientConfig, ClientState)> {
self.clients.iter_mut().find(|(h, _, _)| *h == handle)
}
fn remove(
&mut self,
handle: ClientHandle,
) -> Option<(ClientHandle, ClientConfig, ClientState)> {
let idx = self.clients.iter().position(|(h, _, _)| *h == handle);
idx.map(|i| self.clients.swap_remove(i))
}
fn handle_event(&mut self, event: FrontendEvent) {
match event {
FrontendEvent::Changed(h) => self.changed = Some(h),
FrontendEvent::Created(h, c, s) => {
eprint!("client added ({h}): ");
print_config(&c);
eprint!(" ");
print_state(&s);
eprintln!();
self.clients.push((h, c, s));
}
FrontendEvent::NoSuchClient(h) => {
eprintln!("no such client: {h}");
}
FrontendEvent::State(h, c, s) => {
if let Some((_, config, state)) = self.find_mut(h) {
let old_host = config.hostname.clone().unwrap_or("\"\"".into());
let new_host = c.hostname.clone().unwrap_or("\"\"".into());
if old_host != new_host {
eprintln!(
"client {h}: hostname updated ({} -> {})",
old_host, new_host
);
}
if config.port != c.port {
eprintln!("client {h} changed port: {} -> {}", config.port, c.port);
}
if config.fix_ips != c.fix_ips {
eprintln!("client {h} ips updated: {:?}", c.fix_ips)
}
*config = c;
if state.active ^ s.active {
eprintln!(
"client {h} {}",
if s.active { "activated" } else { "deactivated" }
);
}
*state = s;
}
}
FrontendEvent::Deleted(h) => {
if let Some((h, c, _)) = self.remove(h) {
eprint!("client {h} removed (");
print_config(&c);
eprintln!(")");
}
}
FrontendEvent::PortChanged(p, e) => {
if let Some(e) = e {
eprintln!("failed to change port: {e}");
} else {
eprintln!("changed port to {p}");
}
}
FrontendEvent::Enumerate(clients) => {
self.clients = clients;
self.print_clients();
}
FrontendEvent::Error(e) => {
eprintln!("ERROR: {e}");
}
FrontendEvent::CaptureStatus(s) => {
eprintln!("capture status: {s:?}")
}
FrontendEvent::EmulationStatus(s) => {
eprintln!("emulation status: {s:?}")
}
}
}
fn print_clients(&mut self) {
for (h, c, s) in self.clients.iter() {
eprint!("client {h}: ");
print_config(c);
eprint!(" ");
print_state(s);
eprintln!();
}
}
}
fn prompt() -> io::Result<()> {
eprint!("lan-mouse > ");
std::io::stderr().flush()?;
Ok(())
}
fn print_config(c: &ClientConfig) {
eprint!(
"{}:{} ({}), ips: {:?}",
c.hostname.clone().unwrap_or("(no hostname)".into()),
c.port,
c.pos,
c.fix_ips
);
}
fn print_state(s: &ClientState) {
eprint!("active: {}, dns: {:?}", s.active, s.ips);
}

18
lan-mouse-gtk/Cargo.toml Normal file
View File

@@ -0,0 +1,18 @@
[package]
name = "lan-mouse-gtk"
description = "GTK4 / Libadwaita Frontend for lan-mouse"
version = "0.2.0"
edition = "2021"
license = "GPL-3.0-or-later"
repository = "https://github.com/feschber/lan-mouse"
[dependencies]
gtk = { package = "gtk4", version = "0.9.0", features = ["v4_2"] }
adw = { package = "libadwaita", version = "0.7.0", features = ["v1_1"] }
async-channel = { version = "2.1.1" }
hostname = "0.4.0"
log = "0.4.20"
lan-mouse-ipc = { path = "../lan-mouse-ipc", version = "0.2.0" }
[build-dependencies]
glib-build-tools = { version = "0.20.0" }

8
lan-mouse-gtk/build.rs Normal file
View File

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

View File

@@ -0,0 +1,90 @@
<?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>
<child type="suffix">
<object class="GtkButton" id="dns_button">
<signal name="clicked" handler="handle_request_dns" swapped="true"/>
<!--<property name="icon-name">network-wired-disconnected-symbolic</property>-->
<property name="icon-name">network-wired-symbolic</property>
<property name="valign">center</property>
<property name="halign">end</property>
<property name="tooltip-text" translatable="yes">resolve host</property>
</object>
</child>
<child type="suffix">
<object class="GtkSpinner" id="dns_loading_indicator">
</object>
</child>
<!-- host -->
<child>
<object class="AdwActionRow">
<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="max-width-chars">5</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>
<style><class name="error"/></style>
</object>
</child>
</object>
</child>
</template>
</interface>

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,10 @@
<?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>
</gresource>
<gresource prefix="/de/feschber/LanMouse/icons">
<file compressed="true" preprocess="xml-stripblanks">de.feschber.LanMouse.svg</file>
</gresource>
</gresources>

View File

@@ -0,0 +1,227 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk" version="4.0"/>
<requires lib="libadwaita" version="1.0"/>
<menu id="main-menu">
<item>
<attribute name="label" translatable="yes">_Close window</attribute>
<attribute name="action">window.close</attribute>
</item>
</menu>
<template class="LanMouseWindow" parent="AdwApplicationWindow">
<property name="width-request">600</property>
<property name="height-request">700</property>
<property name="title" translatable="yes">Lan Mouse</property>
<property name="show-menubar">True</property>
<property name="content">
<object class="GtkBox">
<property name="orientation">vertical</property>
<child type="top">
<object class="AdwHeaderBar">
<child type ="end">
<object class="GtkMenuButton">
<property name="icon-name">open-menu-symbolic</property>
<property name="menu-model">main-menu</property>
</object>
</child>
<style>
<class name="flat"/>
</style>
</object>
</child>
<child>
<object class="AdwToastOverlay" id="toast_overlay">
<child>
<object class="AdwStatusPage">
<property name="title" translatable="yes">Lan Mouse</property>
<property name="description" translatable="yes">easily use your mouse and keyboard on multiple computers</property>
<property name="icon-name">de.feschber.LanMouse</property>
<property name="child">
<object class="AdwClamp">
<property name="maximum-size">600</property>
<property name="tightening-threshold">0</property>
<property name="child">
<object class="GtkBox">
<property name="orientation">vertical</property>
<property name="spacing">12</property>
<child>
<object class="AdwPreferencesGroup" id="capture_emulation_group">
<property name="title" translatable="yes">Capture / Emulation Status</property>
<child>
<object class="AdwActionRow" id="capture_status_row">
<property name="title">input capture is disabled</property>
<property name="subtitle">required for outgoing and incoming connections</property>
<property name="icon-name">dialog-warning-symbolic</property>
<child>
<object class="GtkButton" id="input_capture_button">
<property name="child">
<object class="AdwButtonContent">
<property name="icon-name">object-rotate-right-symbolic</property>
<property name="label" translatable="yes">Reenable</property>
</object>
</property>
<signal name="clicked" handler="handle_capture" swapped="true"/>
<property name="valign">center</property>
<style>
<class name="circular"/>
<class name="flat"/>
</style>
</object>
</child>
<style>
<class name="warning"/>
</style>
</object>
</child>
<child>
<object class="AdwActionRow" id="emulation_status_row">
<property name="title">input emulation is disabled</property>
<property name="subtitle">required for incoming connections</property>
<property name="icon-name">dialog-warning-symbolic</property>
<child>
<object class="GtkButton" id="input_emulation_button">
<property name="child">
<object class="AdwButtonContent">
<property name="icon-name">object-rotate-right-symbolic</property>
<property name="label" translatable="yes">Reenable</property>
</object>
</property>
<property name="valign">center</property>
<signal name="clicked" handler="handle_emulation" swapped="true"/>
<style>
<class name="circular"/>
<class name="flat"/>
</style>
</object>
</child>
<child>
</child>
<style>
<class name="warning"/>
</style>
</object>
</child>
</object>
</child>
<child>
<object class="AdwPreferencesGroup">
<property name="title" translatable="yes">General</property>
<!--
<child>
<object class="AdwActionRow">
<property name="title" translatable="yes">enable</property>
<child type="suffix">
<object class="GtkSwitch">
<property name="valign">center</property>
<property name="tooltip-text" translatable="yes">enable</property>
</object>
</child>
</object>
</child>
-->
<child>
<object class="AdwActionRow">
<property name="title">port</property>
<child>
<object class="GtkEntry" id="port_entry">
<property name="max-width-chars">5</property>
<signal name="activate" handler="handle_port_edit_apply" swapped="true"/>
<signal name="changed" handler="handle_port_changed" swapped="true"/>
<!-- <signal name="delete-text" handler="handle_port_changed" swapped="true"/> -->
<!-- <property name="title" translatable="yes">port</property> -->
<property name="placeholder-text">4242</property>
<property name="width-chars">5</property>
<property name="xalign">0.5</property>
<property name="valign">center</property>
<!-- <property name="show-apply-button">True</property> -->
<property name="input-purpose">GTK_INPUT_PURPOSE_DIGITS</property>
</object>
</child>
<child>
<object class="GtkButton" id="port_edit_apply">
<signal name="clicked" handler="handle_port_edit_apply" swapped="true"/>
<property name="icon-name">object-select-symbolic</property>
<property name="valign">center</property>
<property name="visible">false</property>
<property name="name">port-edit-apply</property>
<style><class name="success"/></style>
</object>
</child>
<child>
<object class="GtkButton" id="port_edit_cancel">
<signal name="clicked" handler="handle_port_edit_cancel" swapped="true"/>
<property name="icon-name">process-stop-symbolic</property>
<property name="valign">center</property>
<property name="visible">false</property>
<property name="name">port-edit-cancel</property>
<style><class name="error"/></style>
</object>
</child>
</object>
</child>
<child>
<object class="AdwActionRow">
<property name="title">hostname</property>
<child>
<object class="GtkLabel" id="hostname_label">
<property name="label">&lt;span font_style=&quot;italic&quot; font_weight=&quot;light&quot; foreground=&quot;darkgrey&quot;&gt;could not determine hostname&lt;/span&gt;</property>
<property name="use-markup">true</property>
<property name="valign">center</property>
</object>
</child>
<child>
<object class="GtkButton" id="copy-hostname-button">
<property name="icon-name">edit-copy-symbolic</property>
<property name="valign">center</property>
<signal name="clicked" handler="handle_copy_hostname" swapped="true"/>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="AdwPreferencesGroup">
<property name="title" translatable="yes">Connections</property>
<property name="header-suffix">
<object class="GtkButton">
<signal name="clicked" handler="handle_add_client_pressed" swapped="true"/>
<property name="child">
<object class="AdwButtonContent">
<property name="icon-name">list-add-symbolic</property>
<property name="label" translatable="yes">Add</property>
</object>
</property>
<style>
<class name="flat"/>
</style>
</object>
</property>
<child>
<object class="GtkListBox" id="client_list">
<property name="selection-mode">none</property>
<child type="placeholder">
<object class="AdwActionRow" id="client_placeholder">
<property name="title">No connections!</property>
<property name="subtitle">add a new client via the + button</property>
</object>
</child>
<style>
<class name="boxed-list" />
</style>
</object>
</child>
</object>
</child>
</object>
</property>
</object>
</property>
</object>
</child>
</object>
</child>
</object>
</property>
</template>
</interface>

View File

@@ -0,0 +1,46 @@
mod imp;
use adw::subclass::prelude::*;
use gtk::glib::{self, Object};
use lan_mouse_ipc::{ClientConfig, ClientHandle, ClientState};
glib::wrapper! {
pub struct ClientObject(ObjectSubclass<imp::ClientObject>);
}
impl ClientObject {
pub fn new(handle: ClientHandle, client: ClientConfig, state: ClientState) -> Self {
Object::builder()
.property("handle", handle)
.property("hostname", client.hostname)
.property("port", client.port as u32)
.property("position", client.pos.to_string())
.property("active", state.active)
.property(
"ips",
state
.ips
.iter()
.map(|ip| ip.to_string())
.collect::<Vec<_>>(),
)
.property("resolving", state.resolving)
.build()
}
pub fn get_data(&self) -> ClientData {
self.imp().data.borrow().clone()
}
}
#[derive(Default, Clone)]
pub struct ClientData {
pub handle: ClientHandle,
pub hostname: Option<String>,
pub port: u32,
pub active: bool,
pub position: String,
pub resolving: bool,
pub ips: Vec<String>,
}

View File

@@ -0,0 +1,32 @@
use std::cell::RefCell;
use glib::Properties;
use gtk::glib;
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use lan_mouse_ipc::ClientHandle;
use super::ClientData;
#[derive(Properties, Default)]
#[properties(wrapper_type = super::ClientObject)]
pub struct ClientObject {
#[property(name = "handle", get, set, type = ClientHandle, member = handle)]
#[property(name = "hostname", get, set, type = String, member = hostname)]
#[property(name = "port", get, set, type = u32, member = port, maximum = u16::MAX as u32)]
#[property(name = "active", get, set, type = bool, member = active)]
#[property(name = "position", get, set, type = String, member = position)]
#[property(name = "resolving", get, set, type = bool, member = resolving)]
#[property(name = "ips", get, set, type = Vec<String>, member = ips)]
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,149 @@
mod imp;
use adw::prelude::*;
use adw::subclass::prelude::*;
use gtk::glib::{self, Object};
use lan_mouse_ipc::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 switch_position_binding = client_object
.bind_property("active", &self.imp().enable_switch.get(), "active")
.bidirectional()
.sync_create()
.build();
let hostname_binding = client_object
.bind_property("hostname", &self.imp().hostname.get(), "text")
.transform_to(|_, v: Option<String>| {
if let Some(hostname) = v {
Some(hostname)
} else {
Some("".to_string())
}
})
.transform_from(|_, v: String| {
if v.as_str().trim() == "" {
Some(None)
} else {
Some(Some(v))
}
})
.bidirectional()
.sync_create()
.build();
let title_binding = client_object
.bind_property("hostname", self, "title")
.transform_to(|_, v: Option<String>| {
if let Some(hostname) = v {
Some(hostname)
} else {
Some("<span font_style=\"italic\" font_weight=\"light\" foreground=\"darkgrey\">no hostname!</span>".to_string())
}
})
.sync_create()
.build();
let port_binding = client_object
.bind_property("port", &self.imp().port.get(), "text")
.transform_from(|_, v: String| {
if v.is_empty() {
Some(DEFAULT_PORT as u32)
} else {
Some(v.parse::<u16>().unwrap_or(DEFAULT_PORT) as u32)
}
})
.transform_to(|_, v: u32| {
if v == 4242 {
Some("".to_string())
} else {
Some(v.to_string())
}
})
.bidirectional()
.sync_create()
.build();
let subtitle_binding = client_object
.bind_property("port", self, "subtitle")
.sync_create()
.build();
let position_binding = client_object
.bind_property("position", &self.imp().position.get(), "selected")
.transform_from(|_, v: u32| match v {
1 => Some("right"),
2 => Some("top"),
3 => Some("bottom"),
_ => Some("left"),
})
.transform_to(|_, v: String| match v.as_str() {
"right" => Some(1),
"top" => Some(2u32),
"bottom" => Some(3u32),
_ => Some(0u32),
})
.bidirectional()
.sync_create()
.build();
let resolve_binding = client_object
.bind_property(
"resolving",
&self.imp().dns_loading_indicator.get(),
"spinning",
)
.sync_create()
.build();
let ip_binding = client_object
.bind_property("ips", &self.imp().dns_button.get(), "tooltip-text")
.transform_to(|_, ips: Vec<String>| {
if ips.is_empty() {
Some("no ip addresses associated with this client".into())
} else {
Some(ips.join("\n"))
}
})
.sync_create()
.build();
bindings.push(active_binding);
bindings.push(switch_position_binding);
bindings.push(hostname_binding);
bindings.push(title_binding);
bindings.push(port_binding);
bindings.push(subtitle_binding);
bindings.push(position_binding);
bindings.push(resolve_binding);
bindings.push(ip_binding);
}
pub fn unbind(&self) {
for binding in self.imp().bindings.borrow_mut().drain(..) {
binding.unbind();
}
}
}

View File

@@ -0,0 +1,103 @@
use std::cell::RefCell;
use adw::subclass::prelude::*;
use adw::{prelude::*, ActionRow, ComboRow};
use glib::{subclass::InitializingObject, Binding};
use gtk::glib::clone;
use gtk::glib::subclass::Signal;
use gtk::{glib, Button, CompositeTemplate, Switch};
use std::sync::OnceLock;
#[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 dns_button: TemplateChild<gtk::Button>,
#[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>,
#[template_child]
pub dns_loading_indicator: TemplateChild<gtk::Spinner>,
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";
const ABSTRACT: bool = false;
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(rename_to = row)]
self,
move |button| {
row.handle_client_delete(button);
}
));
}
fn signals() -> &'static [glib::subclass::Signal] {
static SIGNALS: OnceLock<Vec<Signal>> = OnceLock::new();
SIGNALS.get_or_init(|| {
vec![
Signal::builder("request-dns").build(),
Signal::builder("request-update")
.param_types([bool::static_type()])
.build(),
Signal::builder("request-delete").build(),
]
})
}
}
#[gtk::template_callbacks]
impl ClientRow {
#[template_callback]
fn handle_client_set_state(&self, state: bool, _switch: &Switch) -> bool {
log::debug!("state change -> requesting update");
self.obj().emit_by_name::<()>("request-update", &[&state]);
true // dont run default handler
}
#[template_callback]
fn handle_request_dns(&self, _: Button) {
self.obj().emit_by_name::<()>("request-dns", &[]);
}
#[template_callback]
fn handle_client_delete(&self, _button: &Button) {
log::debug!("delete button pressed -> requesting delete");
self.obj().emit_by_name::<()>("request-delete", &[]);
}
}
impl WidgetImpl for ClientRow {}
impl BoxImpl for ClientRow {}
impl ListBoxRowImpl for ClientRow {}
impl PreferencesRowImpl for ClientRow {}
impl ExpanderRowImpl for ClientRow {}

141
lan-mouse-gtk/src/lib.rs Normal file
View File

@@ -0,0 +1,141 @@
mod client_object;
mod client_row;
mod window;
use std::{env, process, str};
use window::Window;
use lan_mouse_ipc::{FrontendEvent, FrontendRequest};
use adw::Application;
use gtk::{
gdk::Display, glib::clone, prelude::*, subclass::prelude::ObjectSubclassIsExt, IconTheme,
};
use gtk::{gio, glib, prelude::ApplicationExt};
use self::client_object::ClientObject;
pub fn run() -> glib::ExitCode {
log::debug!("running gtk frontend");
#[cfg(windows)]
let ret = std::thread::Builder::new()
.stack_size(8 * 1024 * 1024) // https://gitlab.gnome.org/GNOME/gtk/-/commit/52dbb3f372b2c3ea339e879689c1de535ba2c2c3 -> caused crash on windows
.name("gtk".into())
.spawn(gtk_main)
.unwrap()
.join()
.unwrap();
#[cfg(not(windows))]
let ret = gtk_main();
if ret == glib::ExitCode::FAILURE {
log::error!("frontend exited with failure");
} else {
log::info!("frontend exited successfully");
}
ret
}
fn gtk_main() -> glib::ExitCode {
gio::resources_register_include!("lan-mouse.gresource").expect("Failed to register resources.");
let app = Application::builder()
.application_id("de.feschber.LanMouse")
.build();
app.connect_startup(|_| load_icons());
app.connect_activate(build_ui);
let args: Vec<&'static str> = vec![];
app.run_with_args(&args)
}
fn load_icons() {
let display = &Display::default().expect("Could not connect to a display.");
let icon_theme = IconTheme::for_display(display);
icon_theme.add_resource_path("/de/feschber/LanMouse/icons");
}
fn build_ui(app: &Application) {
log::debug!("connecting to lan-mouse-socket");
let (mut frontend_rx, frontend_tx) = match lan_mouse_ipc::connect() {
Ok(conn) => conn,
Err(e) => {
log::error!("{e}");
process::exit(1);
}
};
log::debug!("connected to lan-mouse-socket");
let (sender, receiver) = async_channel::bounded(10);
gio::spawn_blocking(move || {
while let Some(e) = frontend_rx.next_event() {
match e {
Ok(e) => sender.send_blocking(e).unwrap(),
Err(e) => {
log::error!("{e}");
break;
}
}
}
});
let window = Window::new(app, frontend_tx);
glib::spawn_future_local(clone!(
#[weak]
window,
async move {
loop {
let notify = receiver.recv().await.unwrap_or_else(|_| process::exit(1));
match notify {
FrontendEvent::Changed(handle) => {
window.request(FrontendRequest::GetState(handle));
}
FrontendEvent::Created(handle, client, state) => {
window.new_client(handle, client, state);
}
FrontendEvent::Deleted(client) => {
window.delete_client(client);
}
FrontendEvent::State(handle, config, state) => {
window.update_client_config(handle, config);
window.update_client_state(handle, state);
}
FrontendEvent::NoSuchClient(_) => {}
FrontendEvent::Error(e) => {
window.show_toast(e.as_str());
}
FrontendEvent::Enumerate(clients) => {
for (handle, client, state) in clients {
if window.client_idx(handle).is_some() {
window.update_client_config(handle, client);
window.update_client_state(handle, state);
} else {
window.new_client(handle, client, state);
}
}
}
FrontendEvent::PortChanged(port, msg) => {
match msg {
None => window.show_toast(format!("port changed: {port}").as_str()),
Some(msg) => window.show_toast(msg.as_str()),
}
window.imp().set_port(port);
}
FrontendEvent::CaptureStatus(s) => {
window.set_capture(s.into());
}
FrontendEvent::EmulationStatus(s) => {
window.set_emulation(s.into());
}
}
}
}
));
window.present();
}

322
lan-mouse-gtk/src/window.rs Normal file
View File

@@ -0,0 +1,322 @@
mod imp;
use adw::prelude::*;
use adw::subclass::prelude::*;
use glib::{clone, Object};
use gtk::{
gio,
glib::{self, closure_local},
ListBox, NoSelection,
};
use lan_mouse_ipc::{
ClientConfig, ClientHandle, ClientState, FrontendRequest, FrontendRequestWriter, Position,
DEFAULT_PORT,
};
use super::{client_object::ClientObject, 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, conn: FrontendRequestWriter) -> Self {
let window: Self = Object::builder().property("application", app).build();
window
.imp()
.frontend_request_writer
.borrow_mut()
.replace(conn);
window
}
pub fn clients(&self) -> gio::ListStore {
self.imp()
.clients
.borrow()
.clone()
.expect("Could not get clients")
}
fn client_by_idx(&self, idx: u32) -> Option<ClientObject> {
self.clients().item(idx).map(|o| o.downcast().unwrap())
}
fn setup_clients(&self) {
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(rename_to = window)]
self,
#[upgrade_or_panic]
move |obj| {
let client_object = obj
.downcast_ref()
.expect("Expected object of type `ClientObject`.");
let row = window.create_client_row(client_object);
row.connect_closure(
"request-update",
false,
closure_local!(
#[strong]
window,
move |row: ClientRow, active: bool| {
if let Some(client) = window.client_by_idx(row.index() as u32) {
window.request_client_activate(&client, active);
window.request_client_update(&client);
window.request_client_state(&client);
}
}
),
);
row.connect_closure(
"request-delete",
false,
closure_local!(
#[strong]
window,
move |row: ClientRow| {
if let Some(client) = window.client_by_idx(row.index() as u32) {
window.request_client_delete(&client);
}
}
),
);
row.connect_closure(
"request-dns",
false,
closure_local!(
#[strong]
window,
move |row: ClientRow| {
if let Some(client) = window.client_by_idx(row.index() as u32) {
window.request_client_update(&client);
window.request_dns(&client);
window.request_client_state(&client);
}
}
),
);
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("de.feschber.LanMouse"));
}
fn create_client_row(&self, client_object: &ClientObject) -> ClientRow {
let row = ClientRow::new(client_object);
row.bind(client_object);
row
}
pub fn new_client(&self, handle: ClientHandle, client: ClientConfig, state: ClientState) {
let client = ClientObject::new(handle, client, state.clone());
self.clients().append(&client);
self.set_placeholder_visible(false);
self.update_dns_state(handle, !state.ips.is_empty());
}
pub fn client_idx(&self, handle: ClientHandle) -> Option<usize> {
self.clients().iter::<ClientObject>().position(|c| {
if let Ok(c) = c {
c.handle() == handle
} else {
false
}
})
}
pub fn delete_client(&self, handle: ClientHandle) {
let Some(idx) = self.client_idx(handle) else {
log::warn!("could not find client with handle {handle}");
return;
};
self.clients().remove(idx as u32);
if self.clients().n_items() == 0 {
self.set_placeholder_visible(true);
}
}
pub fn update_client_config(&self, handle: ClientHandle, client: ClientConfig) {
let Some(idx) = self.client_idx(handle) else {
log::warn!("could not find client with handle {}", handle);
return;
};
let client_object = self.clients().item(idx as u32).unwrap();
let client_object: &ClientObject = client_object.downcast_ref().unwrap();
let data = client_object.get_data();
/* only change if it actually has changed, otherwise
* the update signal is triggered */
if data.hostname != client.hostname {
client_object.set_hostname(client.hostname.unwrap_or("".into()));
}
if data.port != client.port as u32 {
client_object.set_port(client.port as u32);
}
if data.position != client.pos.to_string() {
client_object.set_position(client.pos.to_string());
}
}
pub fn update_client_state(&self, handle: ClientHandle, state: ClientState) {
let Some(idx) = self.client_idx(handle) else {
log::warn!("could not find client with handle {}", handle);
return;
};
let client_object = self.clients().item(idx as u32).unwrap();
let client_object: &ClientObject = client_object.downcast_ref().unwrap();
let data = client_object.get_data();
if state.active != data.active {
client_object.set_active(state.active);
log::debug!("set active to {}", state.active);
}
if state.resolving != data.resolving {
client_object.set_resolving(state.resolving);
log::debug!("resolving {}: {}", data.handle, state.resolving);
}
self.update_dns_state(handle, !state.ips.is_empty());
let ips = state
.ips
.into_iter()
.map(|ip| ip.to_string())
.collect::<Vec<_>>();
client_object.set_ips(ips);
}
pub fn update_dns_state(&self, handle: ClientHandle, resolved: bool) {
let Some(idx) = self.client_idx(handle) else {
log::warn!("could not find client with handle {}", handle);
return;
};
let list_box: ListBox = self.imp().client_list.get();
let row = list_box.row_at_index(idx as i32).unwrap();
let client_row: ClientRow = row.downcast().expect("expected ClientRow Object");
if resolved {
client_row.imp().dns_button.set_css_classes(&["success"])
} else {
client_row.imp().dns_button.set_css_classes(&["warning"])
}
}
pub fn request_port_change(&self) {
let port = self
.imp()
.port_entry
.get()
.text()
.as_str()
.parse::<u16>()
.unwrap_or(DEFAULT_PORT);
self.request(FrontendRequest::ChangePort(port));
}
pub fn request_capture(&self) {
self.request(FrontendRequest::EnableCapture);
}
pub fn request_emulation(&self) {
self.request(FrontendRequest::EnableEmulation);
}
pub fn request_client_state(&self, client: &ClientObject) {
self.request_client_state_for(client.handle());
}
pub fn request_client_state_for(&self, handle: ClientHandle) {
self.request(FrontendRequest::GetState(handle));
}
pub fn request_client_create(&self) {
self.request(FrontendRequest::Create);
}
pub fn request_dns(&self, client: &ClientObject) {
self.request(FrontendRequest::ResolveDns(client.get_data().handle));
}
pub fn request_client_update(&self, client: &ClientObject) {
let handle = client.handle();
let data = client.get_data();
let position = Position::try_from(data.position.as_str()).expect("invalid position");
let hostname = data.hostname;
let port = data.port as u16;
for event in [
FrontendRequest::UpdateHostname(handle, hostname),
FrontendRequest::UpdatePosition(handle, position),
FrontendRequest::UpdatePort(handle, port),
] {
self.request(event);
}
}
pub fn request_client_activate(&self, client: &ClientObject, active: bool) {
self.request(FrontendRequest::Activate(client.handle(), active));
}
pub fn request_client_delete(&self, client: &ClientObject) {
self.request(FrontendRequest::Delete(client.handle()));
}
pub fn request(&self, request: FrontendRequest) {
let mut requester = self.imp().frontend_request_writer.borrow_mut();
let requester = requester.as_mut().unwrap();
if let Err(e) = requester.request(request) {
log::error!("error sending message: {e}");
};
}
pub fn show_toast(&self, msg: &str) {
let toast = adw::Toast::new(msg);
let toast_overlay = &self.imp().toast_overlay;
toast_overlay.add_toast(toast);
}
pub fn set_capture(&self, active: bool) {
self.imp().capture_active.replace(active);
self.update_capture_emulation_status();
}
pub fn set_emulation(&self, active: bool) {
self.imp().emulation_active.replace(active);
self.update_capture_emulation_status();
}
fn update_capture_emulation_status(&self) {
let capture = self.imp().capture_active.get();
let emulation = self.imp().emulation_active.get();
self.imp().capture_status_row.set_visible(!capture);
self.imp().emulation_status_row.set_visible(!emulation);
self.imp()
.capture_emulation_group
.set_visible(!capture || !emulation);
}
}

View File

@@ -0,0 +1,150 @@
use std::cell::{Cell, RefCell};
use adw::subclass::prelude::*;
use adw::{prelude::*, ActionRow, PreferencesGroup, ToastOverlay};
use glib::subclass::InitializingObject;
use gtk::glib::clone;
use gtk::{gdk, gio, glib, Button, CompositeTemplate, Entry, Label, ListBox};
use lan_mouse_ipc::{FrontendRequestWriter, DEFAULT_PORT};
#[derive(CompositeTemplate, Default)]
#[template(resource = "/de/feschber/LanMouse/window.ui")]
pub struct Window {
#[template_child]
pub port_edit_apply: TemplateChild<Button>,
#[template_child]
pub port_edit_cancel: TemplateChild<Button>,
#[template_child]
pub client_list: TemplateChild<ListBox>,
#[template_child]
pub client_placeholder: TemplateChild<ActionRow>,
#[template_child]
pub port_entry: TemplateChild<Entry>,
#[template_child]
pub hostname_label: TemplateChild<Label>,
#[template_child]
pub toast_overlay: TemplateChild<ToastOverlay>,
#[template_child]
pub capture_emulation_group: TemplateChild<PreferencesGroup>,
#[template_child]
pub capture_status_row: TemplateChild<ActionRow>,
#[template_child]
pub emulation_status_row: TemplateChild<ActionRow>,
#[template_child]
pub input_emulation_button: TemplateChild<Button>,
#[template_child]
pub input_capture_button: TemplateChild<Button>,
pub clients: RefCell<Option<gio::ListStore>>,
pub frontend_request_writer: RefCell<Option<FrontendRequestWriter>>,
pub port: Cell<u16>,
pub capture_active: Cell<bool>,
pub emulation_active: Cell<bool>,
}
#[glib::object_subclass]
impl ObjectSubclass for Window {
// `NAME` needs to match `class` attribute of template
const NAME: &'static str = "LanMouseWindow";
const ABSTRACT: bool = false;
type Type = super::Window;
type ParentType = adw::ApplicationWindow;
fn class_init(klass: &mut Self::Class) {
klass.bind_template();
klass.bind_template_callbacks();
}
fn instance_init(obj: &InitializingObject<Self>) {
obj.init_template();
}
}
#[gtk::template_callbacks]
impl Window {
#[template_callback]
fn handle_add_client_pressed(&self, _button: &Button) {
self.obj().request_client_create();
}
#[template_callback]
fn handle_copy_hostname(&self, button: &Button) {
if let Ok(hostname) = hostname::get() {
let display = gdk::Display::default().unwrap();
let clipboard = display.clipboard();
clipboard.set_text(hostname.to_str().expect("hostname: invalid utf8"));
button.set_icon_name("emblem-ok-symbolic");
button.set_css_classes(&["success"]);
glib::spawn_future_local(clone!(
#[weak]
button,
async move {
glib::timeout_future_seconds(1).await;
button.set_icon_name("edit-copy-symbolic");
button.set_css_classes(&[]);
}
));
}
}
#[template_callback]
fn handle_port_changed(&self, _entry: &Entry) {
self.port_edit_apply.set_visible(true);
self.port_edit_cancel.set_visible(true);
}
#[template_callback]
fn handle_port_edit_apply(&self) {
self.obj().request_port_change();
}
#[template_callback]
fn handle_port_edit_cancel(&self) {
log::debug!("cancel port edit");
self.port_entry
.set_text(self.port.get().to_string().as_str());
self.port_edit_apply.set_visible(false);
self.port_edit_cancel.set_visible(false);
}
#[template_callback]
fn handle_emulation(&self) {
self.obj().request_emulation();
}
#[template_callback]
fn handle_capture(&self) {
self.obj().request_capture();
}
pub fn set_port(&self, port: u16) {
self.port.set(port);
if port == DEFAULT_PORT {
self.port_entry.set_text("");
} else {
self.port_entry.set_text(format!("{port}").as_str());
}
self.port_edit_apply.set_visible(false);
self.port_edit_cancel.set_visible(false);
}
}
impl ObjectImpl for Window {
fn constructed(&self) {
if let Ok(hostname) = hostname::get() {
self.hostname_label
.set_text(hostname.to_str().expect("hostname: invalid utf8"));
}
self.parent_constructed();
self.set_port(DEFAULT_PORT);
let obj = self.obj();
obj.setup_icon();
obj.setup_clients();
}
}
impl WidgetImpl for Window {}
impl WindowImpl for Window {}
impl ApplicationWindowImpl for Window {}
impl AdwApplicationWindowImpl for Window {}

16
lan-mouse-ipc/Cargo.toml Normal file
View File

@@ -0,0 +1,16 @@
[package]
name = "lan-mouse-ipc"
description = "library for communication between lan-mouse service and frontends"
version = "0.2.0"
edition = "2021"
license = "GPL-3.0-or-later"
repository = "https://github.com/feschber/lan-mouse"
[dependencies]
futures = "0.3.30"
log = "0.4.22"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.107"
thiserror = "2.0.0"
tokio = { version = "1.32.0", features = ["net", "io-util", "time"] }
tokio-stream = { version = "0.1.15", features = ["io-util"] }

View File

@@ -0,0 +1,89 @@
use crate::{ConnectionError, FrontendEvent, FrontendRequest, IpcError};
use std::{
cmp::min,
io::{self, prelude::*, BufReader, LineWriter, Lines},
thread,
time::Duration,
};
#[cfg(unix)]
use std::os::unix::net::UnixStream;
#[cfg(windows)]
use std::net::TcpStream;
pub struct FrontendEventReader {
#[cfg(unix)]
lines: Lines<BufReader<UnixStream>>,
#[cfg(windows)]
lines: Lines<BufReader<TcpStream>>,
}
pub struct FrontendRequestWriter {
#[cfg(unix)]
line_writer: LineWriter<UnixStream>,
#[cfg(windows)]
line_writer: LineWriter<TcpStream>,
}
impl FrontendEventReader {
pub fn next_event(&mut self) -> Option<Result<FrontendEvent, IpcError>> {
match self.lines.next()? {
Err(e) => Some(Err(e.into())),
Ok(l) => Some(serde_json::from_str(l.as_str()).map_err(|e| e.into())),
}
}
}
impl FrontendRequestWriter {
pub fn request(&mut self, request: FrontendRequest) -> Result<(), io::Error> {
let mut json = serde_json::to_string(&request).unwrap();
log::debug!("requesting: {json}");
json.push('\n');
self.line_writer.write_all(json.as_bytes())?;
Ok(())
}
}
pub fn connect() -> Result<(FrontendEventReader, FrontendRequestWriter), ConnectionError> {
let rx = wait_for_service()?;
let tx = rx.try_clone()?;
let buf_reader = BufReader::new(rx);
let lines = buf_reader.lines();
let line_writer = LineWriter::new(tx);
let reader = FrontendEventReader { lines };
let writer = FrontendRequestWriter { line_writer };
Ok((reader, writer))
}
/// wait for the lan-mouse socket to come online
#[cfg(unix)]
fn wait_for_service() -> Result<UnixStream, ConnectionError> {
let socket_path = crate::default_socket_path()?;
let mut duration = Duration::from_millis(10);
loop {
if let Ok(stream) = UnixStream::connect(&socket_path) {
break Ok(stream);
}
// a signaling mechanism or inotify could be used to
// improve this
thread::sleep(exponential_back_off(&mut duration));
}
}
#[cfg(windows)]
fn wait_for_service() -> Result<TcpStream, ConnectionError> {
let mut duration = Duration::from_millis(10);
loop {
if let Ok(stream) = TcpStream::connect("127.0.0.1:5252") {
break Ok(stream);
}
thread::sleep(exponential_back_off(&mut duration));
}
}
fn exponential_back_off(duration: &mut Duration) -> Duration {
let new = duration.saturating_mul(2);
*duration = min(new, Duration::from_secs(1));
*duration
}

View File

@@ -0,0 +1,104 @@
use crate::{ConnectionError, FrontendEvent, FrontendRequest, IpcError};
use std::{
cmp::min,
io,
task::{ready, Poll},
time::Duration,
};
use futures::{Stream, StreamExt};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader, ReadHalf, WriteHalf};
use tokio_stream::wrappers::LinesStream;
#[cfg(unix)]
use tokio::net::UnixStream;
#[cfg(windows)]
use tokio::net::TcpStream;
pub struct AsyncFrontendEventReader {
#[cfg(unix)]
lines_stream: LinesStream<BufReader<ReadHalf<UnixStream>>>,
#[cfg(windows)]
lines_stream: LinesStream<BufReader<ReadHalf<TcpStream>>>,
}
pub struct AsyncFrontendRequestWriter {
#[cfg(unix)]
tx: WriteHalf<UnixStream>,
#[cfg(windows)]
tx: WriteHalf<TcpStream>,
}
impl Stream for AsyncFrontendEventReader {
type Item = Result<FrontendEvent, IpcError>;
fn poll_next(
mut self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Option<Self::Item>> {
let line = ready!(self.lines_stream.poll_next_unpin(cx));
let event = line.map(|l| {
l.map_err(Into::<IpcError>::into)
.and_then(|l| serde_json::from_str(l.as_str()).map_err(|e| e.into()))
});
Poll::Ready(event)
}
}
impl AsyncFrontendRequestWriter {
pub async fn request(&mut self, request: FrontendRequest) -> Result<(), io::Error> {
let mut json = serde_json::to_string(&request).unwrap();
log::debug!("requesting: {json}");
json.push('\n');
self.tx.write_all(json.as_bytes()).await?;
Ok(())
}
}
pub async fn connect_async(
) -> Result<(AsyncFrontendEventReader, AsyncFrontendRequestWriter), ConnectionError> {
let stream = wait_for_service().await?;
#[cfg(unix)]
let (rx, tx): (ReadHalf<UnixStream>, WriteHalf<UnixStream>) = tokio::io::split(stream);
#[cfg(windows)]
let (rx, tx): (ReadHalf<TcpStream>, WriteHalf<TcpStream>) = tokio::io::split(stream);
let buf_reader = BufReader::new(rx);
let lines = buf_reader.lines();
let lines_stream = LinesStream::new(lines);
let reader = AsyncFrontendEventReader { lines_stream };
let writer = AsyncFrontendRequestWriter { tx };
Ok((reader, writer))
}
/// wait for the lan-mouse socket to come online
#[cfg(unix)]
async fn wait_for_service() -> Result<UnixStream, ConnectionError> {
let socket_path = crate::default_socket_path()?;
let mut duration = Duration::from_millis(10);
loop {
if let Ok(stream) = UnixStream::connect(&socket_path).await {
break Ok(stream);
}
// a signaling mechanism or inotify could be used to
// improve this
tokio::time::sleep(exponential_back_off(&mut duration)).await;
}
}
#[cfg(windows)]
async fn wait_for_service() -> Result<TcpStream, ConnectionError> {
let mut duration = Duration::from_millis(10);
loop {
if let Ok(stream) = TcpStream::connect("127.0.0.1:5252").await {
break Ok(stream);
}
tokio::time::sleep(exponential_back_off(&mut duration)).await;
}
}
fn exponential_back_off(duration: &mut Duration) -> Duration {
let new = duration.saturating_mul(2);
*duration = min(new, Duration::from_secs(1));
*duration
}

264
lan-mouse-ipc/src/lib.rs Normal file
View File

@@ -0,0 +1,264 @@
use std::{
collections::HashSet,
env::VarError,
fmt::Display,
io,
net::{IpAddr, SocketAddr},
str::FromStr,
};
use thiserror::Error;
#[cfg(unix)]
use std::{
env,
path::{Path, PathBuf},
};
use serde::{Deserialize, Serialize};
mod connect;
mod connect_async;
mod listen;
pub use connect::{connect, FrontendEventReader, FrontendRequestWriter};
pub use connect_async::{connect_async, AsyncFrontendEventReader, AsyncFrontendRequestWriter};
pub use listen::AsyncFrontendListener;
#[derive(Debug, Error)]
pub enum ConnectionError {
#[error(transparent)]
SocketPath(#[from] SocketPathError),
#[error(transparent)]
Io(#[from] io::Error),
}
#[derive(Debug, Error)]
pub enum ListenerCreationError {
#[error("could not determine socket-path: `{0}`")]
SocketPath(#[from] SocketPathError),
#[error("service already running!")]
AlreadyRunning,
#[error("failed to bind lan-mouse socket: `{0}`")]
Bind(io::Error),
}
#[derive(Debug, Error)]
pub enum IpcError {
#[error("io error occured: `{0}`")]
Io(#[from] io::Error),
#[error("invalid json: `{0}`")]
Json(#[from] serde_json::Error),
#[error(transparent)]
Connection(#[from] ConnectionError),
#[error(transparent)]
Listen(#[from] ListenerCreationError),
}
pub const DEFAULT_PORT: u16 = 4242;
#[derive(Debug, Default, Eq, Hash, PartialEq, Clone, Copy, Serialize, Deserialize)]
pub enum Position {
#[default]
Left,
Right,
Top,
Bottom,
}
#[derive(Debug, Error)]
#[error("not a valid position: {pos}")]
pub struct PositionParseError {
pos: String,
}
impl FromStr for Position {
type Err = PositionParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"left" => Ok(Self::Left),
"right" => Ok(Self::Right),
"top" => Ok(Self::Top),
"bottom" => Ok(Self::Bottom),
_ => Err(PositionParseError { pos: s.into() }),
}
}
}
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",
}
)
}
}
impl TryFrom<&str> for Position {
type Error = ();
fn try_from(s: &str) -> Result<Self, Self::Error> {
match s {
"left" => Ok(Position::Left),
"right" => Ok(Position::Right),
"top" => Ok(Position::Top),
"bottom" => Ok(Position::Bottom),
_ => Err(()),
}
}
}
#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
pub struct ClientConfig {
/// hostname of this client
pub hostname: Option<String>,
/// fix ips, determined by the user
pub fix_ips: Vec<IpAddr>,
/// both active_addr and addrs can be None / empty so port needs to be stored seperately
pub port: u16,
/// position of a client on screen
pub pos: Position,
/// enter hook
pub cmd: Option<String>,
}
impl Default for ClientConfig {
fn default() -> Self {
Self {
port: DEFAULT_PORT,
hostname: Default::default(),
fix_ips: Default::default(),
pos: Default::default(),
cmd: None,
}
}
}
pub type ClientHandle = u64;
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct ClientState {
/// events should be sent to and received from the client
pub active: bool,
/// `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>,
/// tracks whether or not the client is responding to pings
pub alive: bool,
/// ips from dns
pub dns_ips: Vec<IpAddr>,
/// all ip addresses associated with a particular client
/// e.g. Laptops usually have at least an ethernet and a wifi port
/// which have different ip addresses
pub ips: HashSet<IpAddr>,
/// client has pressed keys
pub has_pressed_keys: bool,
/// dns resolving in progress
pub resolving: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum FrontendEvent {
/// client state has changed, new state must be requested via [`FrontendRequest::GetState`]
Changed(ClientHandle),
/// a client was created
Created(ClientHandle, ClientConfig, ClientState),
/// no such client
NoSuchClient(ClientHandle),
/// state changed
State(ClientHandle, ClientConfig, ClientState),
/// the client was deleted
Deleted(ClientHandle),
/// new port, reason of failure (if failed)
PortChanged(u16, Option<String>),
/// list of all clients, used for initial state synchronization
Enumerate(Vec<(ClientHandle, ClientConfig, ClientState)>),
/// an error occured
Error(String),
/// capture status
CaptureStatus(Status),
/// emulation status
EmulationStatus(Status),
}
#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
pub enum FrontendRequest {
/// activate/deactivate client
Activate(ClientHandle, bool),
/// add a new client
Create,
/// change the listen port (recreate udp listener)
ChangePort(u16),
/// remove a client
Delete(ClientHandle),
/// request an enumeration of all clients
Enumerate(),
/// resolve dns
ResolveDns(ClientHandle),
/// update hostname
UpdateHostname(ClientHandle, Option<String>),
/// update port
UpdatePort(ClientHandle, u16),
/// update position
UpdatePosition(ClientHandle, Position),
/// update fix-ips
UpdateFixIps(ClientHandle, Vec<IpAddr>),
/// request the state of the given client
GetState(ClientHandle),
/// request reenabling input capture
EnableCapture,
/// request reenabling input emulation
EnableEmulation,
/// synchronize all state
Sync,
}
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize)]
pub enum Status {
#[default]
Disabled,
Enabled,
}
impl From<Status> for bool {
fn from(status: Status) -> Self {
match status {
Status::Enabled => true,
Status::Disabled => false,
}
}
}
#[cfg(unix)]
const LAN_MOUSE_SOCKET_NAME: &str = "lan-mouse-socket.sock";
#[derive(Debug, Error)]
pub enum SocketPathError {
#[error("could not determine $XDG_RUNTIME_DIR: `{0}`")]
XdgRuntimeDirNotFound(VarError),
#[error("could not determine $HOME: `{0}`")]
HomeDirNotFound(VarError),
}
#[cfg(all(unix, not(target_os = "macos")))]
pub fn default_socket_path() -> Result<PathBuf, SocketPathError> {
let xdg_runtime_dir =
env::var("XDG_RUNTIME_DIR").map_err(SocketPathError::XdgRuntimeDirNotFound)?;
Ok(Path::new(xdg_runtime_dir.as_str()).join(LAN_MOUSE_SOCKET_NAME))
}
#[cfg(all(unix, target_os = "macos"))]
pub fn default_socket_path() -> Result<PathBuf, SocketPathError> {
let home = env::var("HOME").map_err(SocketPathError::HomeDirNotFound)?;
Ok(Path::new(home.as_str())
.join("Library")
.join("Caches")
.join(LAN_MOUSE_SOCKET_NAME))
}

147
lan-mouse-ipc/src/listen.rs Normal file
View File

@@ -0,0 +1,147 @@
use futures::{stream::SelectAll, Stream, StreamExt};
#[cfg(unix)]
use std::path::PathBuf;
use std::{
io::ErrorKind,
pin::Pin,
task::{Context, Poll},
};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader, ReadHalf, WriteHalf};
use tokio_stream::wrappers::LinesStream;
#[cfg(unix)]
use tokio::net::UnixListener;
#[cfg(unix)]
use tokio::net::UnixStream;
#[cfg(windows)]
use tokio::net::TcpListener;
#[cfg(windows)]
use tokio::net::TcpStream;
use crate::{FrontendEvent, FrontendRequest, IpcError, ListenerCreationError};
pub struct AsyncFrontendListener {
#[cfg(windows)]
listener: TcpListener,
#[cfg(unix)]
listener: UnixListener,
#[cfg(unix)]
socket_path: PathBuf,
#[cfg(unix)]
line_streams: SelectAll<LinesStream<BufReader<ReadHalf<UnixStream>>>>,
#[cfg(windows)]
line_streams: SelectAll<LinesStream<BufReader<ReadHalf<TcpStream>>>>,
#[cfg(unix)]
tx_streams: Vec<WriteHalf<UnixStream>>,
#[cfg(windows)]
tx_streams: Vec<WriteHalf<TcpStream>>,
}
impl AsyncFrontendListener {
pub async fn new() -> Result<Self, ListenerCreationError> {
#[cfg(unix)]
let (socket_path, listener) = {
let socket_path = crate::default_socket_path()?;
log::debug!("remove socket: {:?}", socket_path);
if socket_path.exists() {
// try to connect to see if some other instance
// of lan-mouse is already running
match UnixStream::connect(&socket_path).await {
// connected -> lan-mouse is already running
Ok(_) => return Err(ListenerCreationError::AlreadyRunning),
// lan-mouse is not running but a socket was left behind
Err(e) => {
log::debug!("{socket_path:?}: {e} - removing left behind socket");
let _ = std::fs::remove_file(&socket_path);
}
}
}
let listener = match UnixListener::bind(&socket_path) {
Ok(ls) => ls,
// some other lan-mouse instance has bound the socket in the meantime
Err(e) if e.kind() == ErrorKind::AddrInUse => {
return Err(ListenerCreationError::AlreadyRunning)
}
Err(e) => return Err(ListenerCreationError::Bind(e)),
};
(socket_path, listener)
};
#[cfg(windows)]
let listener = match TcpListener::bind("127.0.0.1:5252").await {
Ok(ls) => ls,
// some other lan-mouse instance has bound the socket in the meantime
Err(e) if e.kind() == ErrorKind::AddrInUse => {
return Err(ListenerCreationError::AlreadyRunning)
}
Err(e) => return Err(ListenerCreationError::Bind(e)),
};
let adapter = Self {
listener,
#[cfg(unix)]
socket_path,
line_streams: SelectAll::new(),
tx_streams: vec![],
};
Ok(adapter)
}
pub async fn broadcast(&mut self, notify: FrontendEvent) {
// encode event
let mut json = serde_json::to_string(&notify).unwrap();
json.push('\n');
let mut keep = vec![];
// TODO do simultaneously
for tx in self.tx_streams.iter_mut() {
// write len + payload
if tx.write(json.as_bytes()).await.is_err() {
keep.push(false);
continue;
}
keep.push(true);
}
// could not find a better solution because async
let mut keep = keep.into_iter();
self.tx_streams.retain(|_| keep.next().unwrap());
}
}
#[cfg(unix)]
impl Drop for AsyncFrontendListener {
fn drop(&mut self) {
log::debug!("remove socket: {:?}", self.socket_path);
let _ = std::fs::remove_file(&self.socket_path);
}
}
impl Stream for AsyncFrontendListener {
type Item = Result<FrontendRequest, IpcError>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
if let Poll::Ready(Some(Ok(l))) = self.line_streams.poll_next_unpin(cx) {
let request = serde_json::from_str(l.as_str()).map_err(|e| e.into());
return Poll::Ready(Some(request));
}
let mut sync = false;
while let Poll::Ready(Ok((stream, _))) = self.listener.poll_accept(cx) {
let (rx, tx) = tokio::io::split(stream);
let buf_reader = BufReader::new(rx);
let lines = buf_reader.lines();
let lines = LinesStream::new(lines);
self.line_streams.push(lines);
self.tx_streams.push(tx);
sync = true;
}
if sync {
Poll::Ready(Some(Ok(FrontendRequest::Sync)))
} else {
Poll::Pending
}
}
}

View File

@@ -0,0 +1,13 @@
[package]
name = "lan-mouse-proto"
description = "network protocol for lan-mouse"
version = "0.2.0"
edition = "2021"
license = "GPL-3.0-or-later"
repository = "https://github.com/feschber/lan-mouse"
[dependencies]
num_enum = "0.7.2"
thiserror = "2.0.0"
input-event = { path = "../input-event", version = "0.3.0" }
paste = "1.0"

251
lan-mouse-proto/src/lib.rs Normal file
View File

@@ -0,0 +1,251 @@
use input_event::{Event as InputEvent, KeyboardEvent, PointerEvent};
use num_enum::{IntoPrimitive, TryFromPrimitive, TryFromPrimitiveError};
use paste::paste;
use std::{
fmt::{Debug, Display},
mem::size_of,
};
use thiserror::Error;
/// defines the maximum size an encoded event can take up
/// this is currently the pointer motion event
/// type: u8, time: u32, dx: f64, dy: f64
pub const MAX_EVENT_SIZE: usize = size_of::<u8>() + size_of::<u32>() + 2 * size_of::<f64>();
/// error type for protocol violations
#[derive(Debug, Error)]
pub enum ProtocolError {
/// event type does not exist
#[error("invalid event id: `{0}`")]
InvalidEventId(#[from] TryFromPrimitiveError<EventType>),
}
/// main lan-mouse protocol event type
#[derive(Clone, Copy, Debug)]
pub enum ProtoEvent {
/// notify a client that the cursor entered its region
/// [`ProtoEvent::Ack`] with the same serial is used for synchronization between devices
Enter(u32),
/// notify a client that the cursor left its region
/// [`ProtoEvent::Ack`] with the same serial is used for synchronization between devices
Leave(u32),
/// acknowledge of an [`ProtoEvent::Enter`] or [`ProtoEvent::Leave`] event
Ack(u32),
/// Input event
Input(InputEvent),
/// Ping event for tracking unresponsive clients.
/// A client has to respond with [`ProtoEvent::Pong`].
Ping,
/// Response to [`ProtoEvent::Ping`]
Pong,
}
impl Display for ProtoEvent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ProtoEvent::Enter(s) => write!(f, "Enter({s})"),
ProtoEvent::Leave(s) => write!(f, "Leave({s})"),
ProtoEvent::Ack(s) => write!(f, "Ack({s})"),
ProtoEvent::Input(e) => write!(f, "{e}"),
ProtoEvent::Ping => write!(f, "ping"),
ProtoEvent::Pong => write!(f, "pong"),
}
}
}
#[derive(TryFromPrimitive, IntoPrimitive)]
#[repr(u8)]
pub enum EventType {
PointerMotion,
PointerButton,
PointerAxis,
PointerAxisValue120,
KeyboardKey,
KeyboardModifiers,
Ping,
Pong,
Enter,
Leave,
Ack,
}
impl ProtoEvent {
fn event_type(&self) -> EventType {
match self {
ProtoEvent::Input(e) => match e {
InputEvent::Pointer(p) => match p {
PointerEvent::Motion { .. } => EventType::PointerMotion,
PointerEvent::Button { .. } => EventType::PointerButton,
PointerEvent::Axis { .. } => EventType::PointerAxis,
PointerEvent::AxisDiscrete120 { .. } => EventType::PointerAxisValue120,
},
InputEvent::Keyboard(k) => match k {
KeyboardEvent::Key { .. } => EventType::KeyboardKey,
KeyboardEvent::Modifiers { .. } => EventType::KeyboardModifiers,
},
},
ProtoEvent::Ping => EventType::Ping,
ProtoEvent::Pong => EventType::Pong,
ProtoEvent::Enter(_) => EventType::Enter,
ProtoEvent::Leave(_) => EventType::Leave,
ProtoEvent::Ack(_) => EventType::Ack,
}
}
}
impl TryFrom<[u8; MAX_EVENT_SIZE]> for ProtoEvent {
type Error = ProtocolError;
fn try_from(buf: [u8; MAX_EVENT_SIZE]) -> Result<Self, Self::Error> {
let mut buf = &buf[..];
let event_type = decode_u8(&mut buf)?;
match EventType::try_from(event_type)? {
EventType::PointerMotion => {
Ok(Self::Input(InputEvent::Pointer(PointerEvent::Motion {
time: decode_u32(&mut buf)?,
dx: decode_f64(&mut buf)?,
dy: decode_f64(&mut buf)?,
})))
}
EventType::PointerButton => {
Ok(Self::Input(InputEvent::Pointer(PointerEvent::Button {
time: decode_u32(&mut buf)?,
button: decode_u32(&mut buf)?,
state: decode_u32(&mut buf)?,
})))
}
EventType::PointerAxis => Ok(Self::Input(InputEvent::Pointer(PointerEvent::Axis {
time: decode_u32(&mut buf)?,
axis: decode_u8(&mut buf)?,
value: decode_f64(&mut buf)?,
}))),
EventType::PointerAxisValue120 => Ok(Self::Input(InputEvent::Pointer(
PointerEvent::AxisDiscrete120 {
axis: decode_u8(&mut buf)?,
value: decode_i32(&mut buf)?,
},
))),
EventType::KeyboardKey => Ok(Self::Input(InputEvent::Keyboard(KeyboardEvent::Key {
time: decode_u32(&mut buf)?,
key: decode_u32(&mut buf)?,
state: decode_u8(&mut buf)?,
}))),
EventType::KeyboardModifiers => Ok(Self::Input(InputEvent::Keyboard(
KeyboardEvent::Modifiers {
depressed: decode_u32(&mut buf)?,
latched: decode_u32(&mut buf)?,
locked: decode_u32(&mut buf)?,
group: decode_u32(&mut buf)?,
},
))),
EventType::Ping => Ok(Self::Ping),
EventType::Pong => Ok(Self::Pong),
EventType::Enter => Ok(Self::Enter(decode_u32(&mut buf)?)),
EventType::Leave => Ok(Self::Leave(decode_u32(&mut buf)?)),
EventType::Ack => Ok(Self::Ack(decode_u32(&mut buf)?)),
}
}
}
impl From<ProtoEvent> for ([u8; MAX_EVENT_SIZE], usize) {
fn from(event: ProtoEvent) -> Self {
let mut buf = [0u8; MAX_EVENT_SIZE];
let mut len = 0usize;
{
let mut buf = &mut buf[..];
let buf = &mut buf;
let len = &mut len;
encode_u8(buf, len, event.event_type() as u8);
match event {
ProtoEvent::Input(event) => match event {
InputEvent::Pointer(p) => match p {
PointerEvent::Motion { time, dx, dy } => {
encode_u32(buf, len, time);
encode_f64(buf, len, dx);
encode_f64(buf, len, dy);
}
PointerEvent::Button {
time,
button,
state,
} => {
encode_u32(buf, len, time);
encode_u32(buf, len, button);
encode_u32(buf, len, state);
}
PointerEvent::Axis { time, axis, value } => {
encode_u32(buf, len, time);
encode_u8(buf, len, axis);
encode_f64(buf, len, value);
}
PointerEvent::AxisDiscrete120 { axis, value } => {
encode_u8(buf, len, axis);
encode_i32(buf, len, value);
}
},
InputEvent::Keyboard(k) => match k {
KeyboardEvent::Key { time, key, state } => {
encode_u32(buf, len, time);
encode_u32(buf, len, key);
encode_u8(buf, len, state);
}
KeyboardEvent::Modifiers {
depressed,
latched,
locked,
group,
} => {
encode_u32(buf, len, depressed);
encode_u32(buf, len, latched);
encode_u32(buf, len, locked);
encode_u32(buf, len, group);
}
},
},
ProtoEvent::Ping => {}
ProtoEvent::Pong => {}
ProtoEvent::Enter(serial) => encode_u32(buf, len, serial),
ProtoEvent::Leave(serial) => encode_u32(buf, len, serial),
ProtoEvent::Ack(serial) => encode_u32(buf, len, serial),
}
}
(buf, len)
}
}
macro_rules! decode_impl {
($t:ty) => {
paste! {
fn [<decode_ $t>](data: &mut &[u8]) -> Result<$t, ProtocolError> {
let (int_bytes, rest) = data.split_at(size_of::<$t>());
*data = rest;
Ok($t::from_be_bytes(int_bytes.try_into().unwrap()))
}
}
};
}
decode_impl!(u8);
decode_impl!(u32);
decode_impl!(i32);
decode_impl!(f64);
macro_rules! encode_impl {
($t:ty) => {
paste! {
fn [<encode_ $t>](buf: &mut &mut [u8], amt: &mut usize, n: $t) {
let src = n.to_be_bytes();
let data = std::mem::take(buf);
let (int_bytes, rest) = data.split_at_mut(size_of::<$t>());
int_bytes.copy_from_slice(&src);
*amt += size_of::<$t>();
*buf = rest
}
}
};
}
encode_impl!(u8);
encode_impl!(u32);
encode_impl!(i32);
encode_impl!(f64);

43
nix/README.md Normal file
View File

@@ -0,0 +1,43 @@
# Nix Flake Usage
## run
```bash
nix run github:feschber/lan-mouse
# with params
nix run github:feschber/lan-mouse -- --help
```
## home-manager module
add input
```nix
inputs = {
lan-mouse.url = "github:feschber/lan-mouse";
}
```
enable lan-mouse
``` nix
{
inputs,
...
}: {
# add the home manager module
imports = [inputs.lan-mouse.homeManagerModules.default];
programs.lan-mouse = {
enable = true;
# systemd = false;
# package = inputs.lan-mouse.packages.${pkgs.stdenv.hostPlatform.system}.default
# Optional configuration in nix syntax, see config.toml for available options
# settings = { };
};
};
}
```

61
nix/default.nix Normal file
View File

@@ -0,0 +1,61 @@
{
rustPlatform,
lib,
pkgs,
}: let
cargoToml = builtins.fromTOML (builtins.readFile ../Cargo.toml);
pname = cargoToml.package.name;
version = cargoToml.package.version;
in
rustPlatform.buildRustPackage {
pname = pname;
version = version;
nativeBuildInputs = with pkgs; [
git
pkg-config
cmake
makeWrapper
buildPackages.gtk4
];
buildInputs = with pkgs; [
xorg.libX11
gtk4
libadwaita
xorg.libXtst
] ++ lib.optionals stdenv.isDarwin
(with darwin.apple_sdk_11_0.frameworks; [
CoreGraphics
ApplicationServices
]);
src = builtins.path {
name = pname;
path = lib.cleanSource ../.;
};
cargoLock.lockFile = ../Cargo.lock;
# Set Environment Variables
RUST_BACKTRACE = "full";
# Needed to enable support for SVG icons in GTK
postInstall = ''
wrapProgram "$out/bin/lan-mouse" \
--set GDK_PIXBUF_MODULE_FILE ${pkgs.librsvg.out}/lib/gdk-pixbuf-2.0/2.10.0/loaders.cache
install -Dm444 *.desktop -t $out/share/applications
install -Dm444 lan-mouse-gtk/resources/*.svg -t $out/share/icons/hicolor/scalable/apps
'';
meta = with lib; {
description = "Lan Mouse is a mouse and keyboard sharing software";
longDescription = ''
Lan Mouse is a mouse and keyboard sharing software similar to universal-control on Apple devices. It allows for using multiple pcs with a single set of mouse and keyboard. This is also known as a Software KVM switch.
The primary target is Wayland on Linux but Windows and MacOS and Linux on Xorg have partial support as well (see below for more details).
'';
mainProgram = pname;
platforms = platforms.all;
};
}

82
nix/hm-module.nix Normal file
View File

@@ -0,0 +1,82 @@
self: {
config,
pkgs,
lib,
...
}:
with lib; let
cfg = config.programs.lan-mouse;
defaultPackage = self.packages.${pkgs.stdenv.hostPlatform.system}.default;
tomlFormat = pkgs.formats.toml {};
in {
options.programs.lan-mouse = with types; {
enable = mkEnableOption "Whether or not to enable lan-mouse.";
package = mkOption {
type = with types; nullOr package;
default = defaultPackage;
defaultText = literalExpression "inputs.lan-mouse.packages.${pkgs.stdenv.hostPlatform.system}.default";
description = ''
The lan-mouse package to use.
By default, this option will use the `packages.default` as exposed by this flake.
'';
};
systemd = mkOption {
type = types.bool;
default = pkgs.stdenv.isLinux;
description = "Whether to enable to systemd service for lan-mouse on linux.";
};
launchd = mkOption {
type = types.bool;
default = pkgs.stdenv.isDarwin;
description = "Whether to enable to launchd service for lan-mouse on macOS.";
};
settings = lib.mkOption {
inherit (tomlFormat) type;
default = {};
example = builtins.fromTOML (builtins.readFile (self + /config.toml));
description = ''
Optional configuration written to {file}`$XDG_CONFIG_HOME/lan-mouse/config.toml`.
See <https://github.com/feschber/lan-mouse/> for
available options and documentation.
'';
};
};
config = mkIf cfg.enable {
systemd.user.services.lan-mouse = lib.mkIf cfg.systemd {
Unit = {
Description = "Systemd service for Lan Mouse";
Requires = ["graphical-session.target"];
};
Service = {
Type = "simple";
ExecStart = "${cfg.package}/bin/lan-mouse --daemon";
};
Install.WantedBy = [
(lib.mkIf config.wayland.windowManager.hyprland.systemd.enable "hyprland-session.target")
(lib.mkIf config.wayland.windowManager.sway.systemd.enable "sway-session.target")
];
};
launchd.agents.lan-mouse = lib.mkIf cfg.launchd {
enable = true;
config = {
ProgramArguments = [
"${cfg.package}/bin/lan-mouse"
"--daemon"
];
KeepAlive = true;
};
};
home.packages = [
cfg.package
];
xdg.configFile."lan-mouse/config.toml" = lib.mkIf (cfg.settings != {}) {
source = tomlFormat.generate "config.toml" cfg.settings;
};
};
}

BIN
screenshots/dark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

BIN
screenshots/light.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

13
service/lan-mouse.service Normal file
View File

@@ -0,0 +1,13 @@
[Unit]
Description=Lan Mouse
# lan mouse needs an active graphical session
After=graphical-session.target
# make sure the service terminates with the graphical session
BindsTo=graphical-session.target
[Service]
ExecStart=/usr/bin/lan-mouse --daemon
Restart=on-failure
[Install]
WantedBy=graphical-session.target

1
shell.nix Normal file
View File

@@ -0,0 +1 @@
(builtins.getFlake ("git+file://" + toString ./.)).devShells.${builtins.currentSystem}.default

View File

@@ -1,2 +0,0 @@
pub mod consumer;
pub mod producer;

View File

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

View File

@@ -1,18 +0,0 @@
use crate::consumer::EventConsumer;
pub struct LibeiConsumer {}
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,149 +0,0 @@
use crate::{event::{KeyboardEvent, PointerEvent}, consumer::EventConsumer};
use winapi::{
self,
um::winuser::{INPUT, INPUT_MOUSE, LPINPUT, MOUSEEVENTF_MOVE, MOUSEINPUT,
MOUSEEVENTF_LEFTDOWN,
MOUSEEVENTF_RIGHTDOWN,
MOUSEEVENTF_MIDDLEDOWN,
MOUSEEVENTF_LEFTUP,
MOUSEEVENTF_RIGHTUP,
MOUSEEVENTF_MIDDLEUP,
MOUSEEVENTF_WHEEL,
MOUSEEVENTF_HWHEEL, INPUT_KEYBOARD, KEYBDINPUT, KEYEVENTF_SCANCODE, KEYEVENTF_KEYUP,
},
};
use crate::{
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 {
type_: INPUT_MOUSE,
u: std::mem::transmute(mi),
};
winapi::um::winuser::SendInput(
1 as u32,
&mut input as LPINPUT,
std::mem::size_of::<INPUT>() as i32,
);
}
}
fn rel_mouse(dx: i32, dy: i32) {
let mi = MOUSEINPUT {
dx,
dy,
mouseData: 0,
dwFlags: MOUSEEVENTF_MOVE,
time: 0,
dwExtraInfo: 0,
};
send_mouse_input(mi);
}
fn mouse_button(button: u32, state: u32) {
let dw_flags = match state {
0 => match button {
0x110 => MOUSEEVENTF_LEFTUP,
0x111 => MOUSEEVENTF_RIGHTUP,
0x112 => MOUSEEVENTF_MIDDLEUP,
_ => return
}
1 => match button {
0x110 => MOUSEEVENTF_LEFTDOWN,
0x111 => MOUSEEVENTF_RIGHTDOWN,
0x112 => MOUSEEVENTF_MIDDLEDOWN,
_ => return
}
_ => return
};
let mi = MOUSEINPUT {
dx: 0, dy: 0, // no movement
mouseData: 0,
dwFlags: dw_flags,
time: 0,
dwExtraInfo: 0,
};
send_mouse_input(mi);
}
fn scroll(axis: u8, value: f64) {
let event_type = match axis {
0 => MOUSEEVENTF_WHEEL,
1 => MOUSEEVENTF_HWHEEL,
_ => return
};
let mi = MOUSEINPUT {
dx: 0, dy: 0,
mouseData: (-value * 15.0) as i32 as u32,
dwFlags: event_type,
time: 0,
dwExtraInfo: 0,
};
send_mouse_input(mi);
}
fn key_event(key: u32, state: u8) {
let ki = KEYBDINPUT {
wVk: 0,
wScan: key as u16,
dwFlags: KEYEVENTF_SCANCODE | match state {
0 => KEYEVENTF_KEYUP,
1 => 0u32,
_ => return
},
time: 0,
dwExtraInfo: 0,
};
send_keyboard_input(ki);
}
fn send_keyboard_input(ki: KEYBDINPUT) {
unsafe {
let mut input = INPUT {
type_: INPUT_KEYBOARD,
u: std::mem::zeroed(),
};
*input.u.ki_mut() = ki;
winapi::um::winuser::SendInput(1 as u32, &mut input, std::mem::size_of::<INPUT>() as i32);
}
}

View File

@@ -1,314 +0,0 @@
use wayland_client::WEnum;
use crate::client::{ClientHandle, ClientEvent};
use crate::consumer::EventConsumer;
use std::collections::HashMap;
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,
};
use wayland_protocols_misc::zwp_virtual_keyboard_v1::client::{
zwp_virtual_keyboard_manager_v1::ZwpVirtualKeyboardManagerV1 as VkManager,
zwp_virtual_keyboard_v1::ZwpVirtualKeyboardV1 as Vk,
};
use wayland_protocols_plasma::fake_input::client::org_kde_kwin_fake_input::OrgKdeKwinFakeInput;
use wayland_client::{
delegate_noop,
globals::{registry_queue_init, GlobalListContents},
protocol::{wl_registry, wl_seat},
Connection, Dispatch, EventQueue, QueueHandle,
};
use crate::event::{Event, KeyboardEvent, PointerEvent};
enum VirtualInputManager {
Wlroots { vpm: VpManager, vkm: VkManager },
Kde { fake_input: OrgKdeKwinFakeInput },
}
struct State {
keymap: Option<(u32, OwnedFd, u32)>,
input_for_client: HashMap<ClientHandle, VirtualInput>,
seat: wl_seat::WlSeat,
virtual_input_manager: VirtualInputManager,
qh: QueueHandle<Self>,
}
// App State, implements Dispatch event handlers
pub(crate) struct WlrootsConsumer {
state: State,
queue: EventQueue<State>,
}
impl WlrootsConsumer {
pub fn new() -> Result<Self> {
let conn = Connection::connect_to_env().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, ());
let virtual_input_manager = match (vpm, vkm, fake_input) {
(Ok(vpm), Ok(vkm), _) => VirtualInputManager::Wlroots { vpm, vkm },
(_, _, Ok(fake_input)) => {
fake_input.authenticate(
"lan-mouse".into(),
"Allow remote clients to control this device".into(),
);
VirtualInputManager::Kde { fake_input }
}
(Err(e1), Err(e2), Err(e3)) => {
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!()
}
};
let input_for_client: HashMap<ClientHandle, VirtualInput> = HashMap::new();
let mut consumer = WlrootsConsumer {
state: State {
keymap: None,
input_for_client,
seat,
virtual_input_manager,
qh,
},
queue,
};
while consumer.state.keymap.is_none() {
consumer.queue.blocking_dispatch(&mut consumer.state).unwrap();
}
// 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)
}
}
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, ());
// 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");
}
let vinput = VirtualInput::Wlroots { pointer, keyboard };
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, 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 },
}
impl VirtualInput {
fn consume_event(&self, event: Event) -> Result<(),()> {
match event {
Event::Pointer(e) => match e {
PointerEvent::Motion {
time,
relative_x,
relative_y,
} => match self {
VirtualInput::Wlroots {
pointer,
keyboard: _,
} => {
pointer.motion(time, relative_x, relative_y);
}
VirtualInput::Kde { fake_input } => {
fake_input.pointer_motion(relative_y, relative_y);
}
},
PointerEvent::Button {
time,
button,
state,
} => {
let state: ButtonState = state.try_into()?;
match self {
VirtualInput::Wlroots {
pointer,
keyboard: _,
} => {
pointer.button(time, button, state);
}
VirtualInput::Kde { fake_input } => {
fake_input.button(button, state as u32);
}
}
}
PointerEvent::Axis { time, axis, value } => {
let axis: Axis = (axis as u32).try_into()?;
match self {
VirtualInput::Wlroots {
pointer,
keyboard: _,
} => {
pointer.axis(time, axis, value);
pointer.frame();
}
VirtualInput::Kde { fake_input } => {
fake_input.axis(axis as u32, value);
}
}
}
PointerEvent::Frame {} => match self {
VirtualInput::Wlroots {
pointer,
keyboard: _,
} => {
pointer.frame();
}
VirtualInput::Kde { fake_input: _ } => {}
},
},
Event::Keyboard(e) => match e {
KeyboardEvent::Key { time, key, state } => match self {
VirtualInput::Wlroots {
pointer: _,
keyboard,
} => {
keyboard.key(time, key, state as u32);
}
VirtualInput::Kde { fake_input } => {
fake_input.keyboard_key(key, state as u32);
}
},
KeyboardEvent::Modifiers {
mods_depressed,
mods_latched,
mods_locked,
group,
} => match self {
VirtualInput::Wlroots {
pointer: _,
keyboard,
} => {
keyboard.modifiers(mods_depressed, mods_latched, mods_locked, group);
}
VirtualInput::Kde { fake_input: _ } => {}
},
},
event => panic!("unknown event type {event:?}"),
}
Ok(())
}
}
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 State {
fn event(
_: &mut State,
_: &wl_registry::WlRegistry,
_: wl_registry::Event,
_: &GlobalListContents,
_: &Connection,
_: &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,57 +0,0 @@
use std::ptr;
use x11::{xlib, xtest};
use crate::{
client::ClientHandle,
event::Event, consumer::EventConsumer,
};
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);
}
}
}
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,
} => {
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(_) => {}
_ => {}
}
}
fn notify(&mut self, _: crate::client::ClientEvent) {
// for our purposes it does not matter what client sent the event
}
}

View File

@@ -1,15 +0,0 @@
use crate::consumer::EventConsumer;
pub struct DesktopPortalConsumer {}
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,6 +0,0 @@
#[cfg(all(unix, feature = "wayland"))]
pub mod wayland;
#[cfg(windows)]
pub mod windows;
#[cfg(all(unix, feature = "x11"))]
pub mod x11;

View File

@@ -1,687 +0,0 @@
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,
};
use wayland_protocols::wp::{
keyboard_shortcuts_inhibit::zv1::client::{
zwp_keyboard_shortcuts_inhibit_manager_v1::ZwpKeyboardShortcutsInhibitManagerV1,
zwp_keyboard_shortcuts_inhibitor_v1::ZwpKeyboardShortcutsInhibitorV1,
},
pointer_constraints::zv1::client::{
zwp_locked_pointer_v1::ZwpLockedPointerV1,
zwp_pointer_constraints_v1::{Lifetime, ZwpPointerConstraintsV1},
},
relative_pointer::zv1::client::{
zwp_relative_pointer_manager_v1::ZwpRelativePointerManagerV1,
zwp_relative_pointer_v1::{self, ZwpRelativePointerV1},
},
};
use wayland_protocols_wlr::layer_shell::v1::client::{
zwlr_layer_shell_v1::{Layer, ZwlrLayerShellV1},
zwlr_layer_surface_v1::{self, Anchor, KeyboardInteractivity, ZwlrLayerSurfaceV1},
};
use wayland_client::{
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,
},
Connection, Dispatch, DispatchError, QueueHandle, WEnum, EventQueue,
};
use tempfile;
use crate::event::{Event, KeyboardEvent, PointerEvent};
struct Globals {
compositor: wl_compositor::WlCompositor,
pointer_constraints: ZwpPointerConstraintsV1,
relative_pointer_manager: ZwpRelativePointerManagerV1,
shortcut_inhibit_manager: ZwpKeyboardShortcutsInhibitManagerV1,
seat: wl_seat::WlSeat,
shm: wl_shm::WlShm,
layer_shell: ZwlrLayerShellV1,
}
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,
wayland_fd: RawFd,
read_guard: Option<ReadEventsGuard>,
qh: QueueHandle<Self>,
pending_events: Vec<(ClientHandle, Event)>,
}
pub struct WaylandEventProducer {
state: State,
queue: EventQueue<State>,
}
struct Window {
buffer: wl_buffer::WlBuffer,
surface: wl_surface::WlSurface,
layer_surface: ZwlrLayerSurfaceV1,
}
impl Window {
fn new(g: &Globals, qh: &QueueHandle<State>, pos: Position) -> Window {
let (width, height) = (1, 1440);
let mut file = tempfile::tempfile().unwrap();
draw(&mut file, (width, height));
let pool = g
.shm
.create_pool(file.as_raw_fd(), (width * height * 4) as i32, qh, ());
let buffer = pool.create_buffer(
0,
width as i32,
height as i32,
(width * 4) as i32,
wl_shm::Format::Argb8888,
qh,
(),
);
let surface = g.compositor.create_surface(qh, ());
let layer_surface = g.layer_shell.get_layer_surface(
&surface,
None,
Layer::Top,
"LAN Mouse Sharing".into(),
qh,
(),
);
let anchor = match pos {
Position::Left => Anchor::Left,
Position::Right => Anchor::Right,
Position::Top => Anchor::Top,
Position::Bottom => Anchor::Bottom,
};
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);
surface.set_input_region(None);
surface.commit();
Window {
buffer,
surface,
layer_surface,
}
}
}
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();
}
}
}
impl WaylandEventProducer {
pub fn new() -> Result<Self> {
let conn = Connection::connect_to_env().expect("could not connect to wayland compositor");
let (g, queue) =
registry_queue_init::<State>(&conn).expect("failed to initialize wl_registry");
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 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 g = Globals {
compositor,
shm,
layer_shell,
seat,
pointer_constraints,
relative_pointer_manager,
shortcut_inhibit_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();
let read_guard = Some(read_guard);
Ok(WaylandEventProducer {
queue,
state: State {
g,
pointer_lock: None,
rel_pointer: None,
shortcut_inhibitor: None,
client_for_window: Vec::new(),
focused: None,
qh,
wayland_fd,
read_guard,
pending_events: vec![],
}
})
}
}
impl State {
fn grab(
&mut self,
surface: &wl_surface::WlSurface,
pointer: &wl_pointer::WlPointer,
serial: u32,
qh: &QueueHandle<State>,
) {
let (window, _) = self.focused.as_ref().unwrap();
// hide the cursor
pointer.set_cursor(serial, None, 0, 0);
// capture input
window
.layer_surface
.set_keyboard_interactivity(KeyboardInteractivity::Exclusive);
window.surface.commit();
// lock pointer
if self.pointer_lock.is_none() {
self.pointer_lock = Some(self.g.pointer_constraints.lock_pointer(
surface,
pointer,
None,
Lifetime::Persistent,
qh,
(),
));
}
// request relative input
if self.rel_pointer.is_none() {
self.rel_pointer = Some(self.g.relative_pointer_manager.get_relative_pointer(
pointer,
qh,
(),
));
}
// capture modifier keys
if self.shortcut_inhibitor.is_none() {
self.shortcut_inhibitor = Some(self.g.shortcut_inhibit_manager.inhibit_shortcuts(
surface,
&self.g.seat,
qh,
(),
));
}
}
fn ungrab(&mut self) {
// get focused client
let (window, _client) = match self.focused.as_ref() {
Some(focused) => focused,
None => return,
};
// ungrab surface
window
.layer_surface
.set_keyboard_interactivity(KeyboardInteractivity::None);
window.surface.commit();
// destroy pointer lock
if let Some(pointer_lock) = &self.pointer_lock {
pointer_lock.destroy();
self.pointer_lock = None;
}
// destroy relative input
if let Some(rel_pointer) = &self.rel_pointer {
rel_pointer.destroy();
self.rel_pointer = None;
}
// destroy shortcut inhibitor
if let Some(shortcut_inhibitor) = &self.shortcut_inhibitor {
shortcut_inhibitor.destroy();
self.shortcut_inhibitor = None;
}
}
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));
}
}
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);
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,
event: <wl_seat::WlSeat as wayland_client::Proxy>::Event,
_: &(),
_: &Connection,
qh: &QueueHandle<Self>,
) {
if let wl_seat::Event::Capabilities {
capabilities: WEnum::Value(capabilities),
} = event
{
if capabilities.contains(wl_seat::Capability::Pointer) {
seat.get_pointer(qh, ());
}
if capabilities.contains(wl_seat::Capability::Keyboard) {
seat.get_keyboard(qh, ());
}
}
}
}
impl Dispatch<wl_pointer::WlPointer, ()> for State {
fn event(
app: &mut Self,
pointer: &wl_pointer::WlPointer,
event: <wl_pointer::WlPointer as wayland_client::Proxy>::Event,
_: &(),
_: &Connection,
qh: &QueueHandle<Self>,
) {
match event {
wl_pointer::Event::Enter {
serial,
surface,
surface_x: _,
surface_y: _,
} => {
// get client corresponding to the focused surface
log::trace!("produce: enter()");
{
let (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);
}
let (_, client) = app
.client_for_window
.iter()
.find(|(w, _c)| w.surface == surface)
.unwrap();
app.pending_events.push((*client, Event::Release()));
}
wl_pointer::Event::Leave { .. } => {
log::trace!("produce: leave()");
app.ungrab();
}
wl_pointer::Event::Button {
serial: _,
time,
button,
state,
} => {
log::trace!("produce: button()");
let (_, client) = app.focused.as_ref().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.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.pending_events.push((
*client,
Event::Pointer(PointerEvent::Frame {}),
));
}
_ => {}
}
}
}
impl Dispatch<wl_keyboard::WlKeyboard, ()> for State {
fn event(
app: &mut Self,
_: &wl_keyboard::WlKeyboard,
event: wl_keyboard::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
let (_window, client) = match &app.focused {
Some(focused) => (Some(&focused.0), Some(&focused.1)),
None => (None, None),
};
match event {
wl_keyboard::Event::Key {
serial: _,
time,
key,
state,
} => {
if let Some(client) = client {
app.pending_events.push((
*client,
Event::Keyboard(KeyboardEvent::Key {
time,
key,
state: u32::from(state) as u8,
}),
));
}
}
wl_keyboard::Event::Modifiers {
serial: _,
mods_depressed,
mods_latched,
mods_locked,
group,
} => {
if let Some(client) = client {
app.pending_events.push((
*client,
Event::Keyboard(KeyboardEvent::Modifiers {
mods_depressed,
mods_latched,
mods_locked,
group,
}),
));
}
if mods_depressed == 77 {
// ctrl shift super alt
app.ungrab();
}
}
wl_keyboard::Event::Keymap {
format: _,
fd,
size: _,
} => {
let fd = unsafe { &File::from_raw_fd(fd.as_raw_fd()) };
let _mmap = unsafe { MmapOptions::new().map_copy(fd).unwrap() };
// TODO keymap
}
_ => (),
}
}
}
impl Dispatch<ZwpRelativePointerV1, ()> for State {
fn event(
app: &mut Self,
_: &ZwpRelativePointerV1,
event: <ZwpRelativePointerV1 as wayland_client::Proxy>::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
if let zwp_relative_pointer_v1::Event::RelativeMotion {
utime_hi,
utime_lo,
dx: _,
dy: _,
dx_unaccel: surface_x,
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.pending_events.push((
*client,
Event::Pointer(PointerEvent::Motion {
time,
relative_x: surface_x,
relative_y: surface_y,
}),
));
}
}
}
}
impl Dispatch<ZwlrLayerSurfaceV1, ()> for State {
fn event(
app: &mut Self,
layer_surface: &ZwlrLayerSurfaceV1,
event: <ZwlrLayerSurfaceV1 as wayland_client::Proxy>::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
if let zwlr_layer_surface_v1::Event::Configure { serial, .. } = event {
let (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.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 State {
fn event(
_state: &mut Self,
_proxy: &wl_registry::WlRegistry,
_event: <wl_registry::WlRegistry as wayland_client::Proxy>::Event,
_data: &GlobalListContents,
_conn: &Connection,
_qhandle: &QueueHandle<Self>,
) {
}
}
// don't emit any events
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!(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,58 +0,0 @@
use std::vec::Drain;
use mio::{Token, Registry};
use mio::event::Source;
use std::io::Result;
use crate::{
client::{ClientHandle, ClientEvent},
event::Event,
producer::EventProducer,
};
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,55 +0,0 @@
use std::vec::Drain;
use mio::{Token, Registry};
use mio::event::Source;
use std::io::Result;
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) {}
}

44
src/capture_test.rs Normal file
View File

@@ -0,0 +1,44 @@
use crate::config::Config;
use futures::StreamExt;
use input_capture::{self, CaptureError, CaptureEvent, InputCapture, InputCaptureError, Position};
use input_event::{Event, KeyboardEvent};
pub async fn run(config: Config) -> Result<(), InputCaptureError> {
log::info!("running input capture test");
log::info!("creating input capture");
let backend = config.capture_backend.map(|b| b.into());
loop {
let mut input_capture = InputCapture::new(backend).await?;
log::info!("creating clients");
input_capture.create(0, Position::Left).await?;
input_capture.create(4, Position::Left).await?;
input_capture.create(1, Position::Right).await?;
input_capture.create(2, Position::Top).await?;
input_capture.create(3, Position::Bottom).await?;
if let Err(e) = do_capture(&mut input_capture).await {
log::warn!("{e} - recreating capture");
}
let _ = input_capture.terminate().await;
}
}
async fn do_capture(input_capture: &mut InputCapture) -> Result<(), CaptureError> {
loop {
let (client, event) = input_capture
.next()
.await
.ok_or(CaptureError::EndOfStream)??;
let pos = match client {
0 | 4 => Position::Left,
1 => Position::Right,
2 => Position::Top,
3 => Position::Bottom,
_ => panic!(),
};
log::info!("position: {client} ({pos}), event: {event}");
if let CaptureEvent::Input(Event::Keyboard(KeyboardEvent::Key { key: 1, .. })) = event {
input_capture.release().await?;
break Ok(());
}
}
}

View File

@@ -1,190 +1,74 @@
use std::{net::SocketAddr, collections::{HashSet, hash_set::Iter}, fmt::Display, time::{Instant, Duration}, iter::Cloned};
use std::net::SocketAddr;
use serde::{Serialize, Deserialize};
use slab::Slab;
#[derive(Debug, Eq, Hash, PartialEq, Clone, Copy, Serialize, Deserialize)]
pub enum Position {
Left,
Right,
Top,
Bottom,
}
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",
})
}
}
#[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;
use lan_mouse_ipc::{ClientConfig, ClientHandle, ClientState, Position};
#[derive(Default)]
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,
clients: Slab<(ClientConfig, ClientState)>,
}
impl ClientManager {
pub fn new() -> Self {
Self {
clients: vec![],
next_client_id: 0,
last_ping: vec![],
last_seen: vec![],
last_replied: vec![],
}
}
/// 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
}
/// 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);
}
}
/// remove socket address from the given client
pub fn remove_addr(&mut self, client: ClientHandle, addr: SocketAddr) {
if let Some(client) = self.get_mut(client) {
client.addrs.remove(&addr);
}
}
pub fn set_default_addr(&mut self, client: ClientHandle, addr: SocketAddr) {
if let Some(client) = self.get_mut(client) {
client.active_addr = Some(addr)
}
}
/// update the position of a client
pub fn update_pos(&mut self, client: ClientHandle, pos: Position) {
if let Some(client) = self.get_mut(client) {
client.pos = pos;
}
}
pub fn get_active_addr(&self, client: ClientHandle) -> Option<SocketAddr> {
self.get(client)?.active_addr
}
pub fn get_addrs(&self, client: ClientHandle) -> Option<Cloned<Iter<'_, SocketAddr>>> {
Some(self.get(client)?.addrs.iter().cloned())
}
pub fn last_ping(&self, client: ClientHandle) -> Option<Duration> {
let last_ping = self.last_ping
.iter()
.find(|(c,_)| *c == client)
.unwrap().1;
last_ping.map(|p| p.elapsed())
}
pub fn last_seen(&self, client: ClientHandle) -> Option<Duration> {
let last_seen = self.last_seen
.iter()
.find(|(c, _)| *c == client)
.unwrap().1;
last_seen.map(|t| t.elapsed())
}
pub fn last_replied(&self, client: ClientHandle) -> Option<Duration> {
let last_replied = self.last_replied
.iter()
.find(|(c, _)| *c == client)
.unwrap().1;
last_replied.map(|t| t.elapsed())
}
pub fn reset_last_ping(&mut self, client: ClientHandle) {
self.last_ping
.iter_mut()
.find(|(c, _)| *c == client)
.unwrap().1 = Some(Instant::now());
}
pub fn reset_last_seen(&mut self, client: ClientHandle) {
self.last_seen
.iter_mut()
.find(|(c, _)| *c == client)
.unwrap().1 = Some(Instant::now());
}
pub fn reset_last_replied(&mut self, client: ClientHandle) {
self.last_replied
.iter_mut()
.find(|(c, _)| *c == client)
.unwrap().1 = Some(Instant::now());
pub fn add_client(&mut self) -> ClientHandle {
self.clients.insert(Default::default()) as ClientHandle
}
/// find a client by its address
pub fn get_client(&self, addr: SocketAddr) -> Option<ClientHandle> {
// since there shouldn't be more than a handful of clients at any given
// time this is likely faster than using a HashMap
self.clients
.iter()
.find(|c| c.addrs.contains(&addr))
.map(|c| c.handle)
.find_map(|(k, (_, s))| {
if s.active && s.ips.contains(&addr.ip()) {
Some(k)
} else {
None
}
})
.map(|p| p as ClientHandle)
}
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> {
pub fn find_client(&self, pos: Position) -> Option<ClientHandle> {
self.clients
.iter()
.find(|c| c.handle == client)
.find_map(|(k, (c, s))| {
if s.active && c.pos == pos {
Some(k)
} else {
None
}
})
.map(|p| p as ClientHandle)
}
fn get_mut<'a>(&'a mut self, client: ClientHandle) -> Option<&'a mut Client> {
self.clients
.iter_mut()
.find(|c| c.handle == client)
/// remove a client from the list
pub fn remove_client(&mut self, client: ClientHandle) -> Option<(ClientConfig, ClientState)> {
// remove id from occupied ids
self.clients.try_remove(client as usize)
}
// returns an immutable reference to the client state corresponding to `client`
pub fn get(&self, handle: ClientHandle) -> Option<&(ClientConfig, ClientState)> {
self.clients.get(handle as usize)
}
/// returns a mutable reference to the client state corresponding to `client`
pub fn get_mut(&mut self, handle: ClientHandle) -> Option<&mut (ClientConfig, ClientState)> {
self.clients.get_mut(handle as usize)
}
pub fn get_client_states(
&self,
) -> impl Iterator<Item = (ClientHandle, &(ClientConfig, ClientState))> {
self.clients.iter().map(|(k, v)| (k as ClientHandle, v))
}
pub fn get_client_states_mut(
&mut self,
) -> impl Iterator<Item = (ClientHandle, &mut (ClientConfig, ClientState))> {
self.clients.iter_mut().map(|(k, v)| (k as ClientHandle, v))
}
}

View File

@@ -1,118 +1,316 @@
use clap::{Parser, ValueEnum};
use serde::{Deserialize, Serialize};
use core::fmt;
use std::collections::HashSet;
use std::net::{IpAddr, SocketAddr};
use std::{error::Error, fs};
use std::env;
use std::env::{self, VarError};
use std::fmt::Display;
use std::fs;
use std::net::IpAddr;
use std::{collections::HashSet, io};
use thiserror::Error;
use toml;
use crate::client::Position;
use crate::dns;
use lan_mouse_ipc::{Position, DEFAULT_PORT};
pub const DEFAULT_PORT: u16 = 4242;
use input_event::scancode::{
self,
Linux::{KeyLeftAlt, KeyLeftCtrl, KeyLeftMeta, KeyLeftShift},
};
#[derive(Serialize, Deserialize, Debug)]
pub struct ConfigToml {
pub capture_backend: Option<CaptureBackend>,
pub emulation_backend: Option<EmulationBackend>,
pub port: Option<u16>,
pub frontend: Option<String>,
pub left: Option<Client>,
pub right: Option<Client>,
pub top: Option<Client>,
pub bottom: Option<Client>,
pub frontend: Option<Frontend>,
pub release_bind: Option<Vec<scancode::Linux>>,
pub left: Option<TomlClient>,
pub right: Option<TomlClient>,
pub top: Option<TomlClient>,
pub bottom: Option<TomlClient>,
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
pub struct Client {
pub struct TomlClient {
pub capture_backend: Option<CaptureBackend>,
pub hostname: Option<String>,
pub host_name: Option<String>,
pub ips: Option<Vec<IpAddr>>,
pub port: Option<u16>,
pub activate_on_startup: Option<bool>,
pub enter_hook: Option<String>,
}
#[derive(Debug, Clone)]
struct MissingParameter {
arg: &'static str,
}
impl fmt::Display for MissingParameter {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Missing a parameter for argument: {}", self.arg)
}
}
impl Error for MissingParameter {}
impl ConfigToml {
pub fn new(path: &str) -> Result<ConfigToml, Box<dyn Error>> {
pub fn new(path: &str) -> Result<ConfigToml, ConfigError> {
let config = fs::read_to_string(path)?;
log::info!("using config: \"{path}\"");
Ok(toml::from_str::<_>(&config)?)
}
}
fn find_arg(key: &'static str) -> Result<Option<String>, MissingParameter> {
let args: Vec<String> = env::args().collect();
#[derive(Parser, Debug)]
#[command(author, version=env!("GIT_DESCRIBE"), about, long_about = None)]
struct CliArgs {
/// the listen port for lan-mouse
#[arg(short, long)]
port: Option<u16>,
for (i, arg) in args.iter().enumerate() {
if arg != key {
continue;
}
match args.get(i+1) {
None => return Err(MissingParameter { arg: key }),
Some(arg) => return Ok(Some(arg.clone())),
};
}
Ok(None)
/// the frontend to use [cli | gtk]
#[arg(short, long)]
frontend: Option<Frontend>,
/// non-default config file location
#[arg(short, long)]
config: Option<String>,
/// run only the service as a daemon without the frontend
#[arg(short, long)]
daemon: bool,
/// test input capture
#[arg(long)]
test_capture: bool,
/// test input emulation
#[arg(long)]
test_emulation: bool,
/// capture backend override
#[arg(long)]
capture_backend: Option<CaptureBackend>,
/// emulation backend override
#[arg(long)]
emulation_backend: Option<EmulationBackend>,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ValueEnum)]
pub enum CaptureBackend {
#[cfg(all(unix, feature = "libei_capture", not(target_os = "macos")))]
#[serde(rename = "input-capture-portal")]
InputCapturePortal,
#[cfg(all(unix, feature = "layer_shell_capture", not(target_os = "macos")))]
#[serde(rename = "layer-shell")]
LayerShell,
#[cfg(all(unix, feature = "x11_capture", not(target_os = "macos")))]
#[serde(rename = "x11")]
X11,
#[cfg(windows)]
#[serde(rename = "windows")]
Windows,
#[cfg(target_os = "macos")]
#[serde(rename = "macos")]
MacOs,
#[serde(rename = "dummy")]
Dummy,
}
impl Display for CaptureBackend {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
#[cfg(all(unix, feature = "libei_capture", not(target_os = "macos")))]
CaptureBackend::InputCapturePortal => write!(f, "input-capture-portal"),
#[cfg(all(unix, feature = "layer_shell_capture", not(target_os = "macos")))]
CaptureBackend::LayerShell => write!(f, "layer-shell"),
#[cfg(all(unix, feature = "x11_capture", not(target_os = "macos")))]
CaptureBackend::X11 => write!(f, "X11"),
#[cfg(windows)]
CaptureBackend::Windows => write!(f, "windows"),
#[cfg(target_os = "macos")]
CaptureBackend::MacOs => write!(f, "MacOS"),
CaptureBackend::Dummy => write!(f, "dummy"),
}
}
}
impl From<CaptureBackend> for input_capture::Backend {
fn from(backend: CaptureBackend) -> Self {
match backend {
#[cfg(all(unix, feature = "libei_capture", not(target_os = "macos")))]
CaptureBackend::InputCapturePortal => Self::InputCapturePortal,
#[cfg(all(unix, feature = "layer_shell_capture", not(target_os = "macos")))]
CaptureBackend::LayerShell => Self::LayerShell,
#[cfg(all(unix, feature = "x11_capture", not(target_os = "macos")))]
CaptureBackend::X11 => Self::X11,
#[cfg(windows)]
CaptureBackend::Windows => Self::Windows,
#[cfg(target_os = "macos")]
CaptureBackend::MacOs => Self::MacOs,
CaptureBackend::Dummy => Self::Dummy,
}
}
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ValueEnum)]
pub enum EmulationBackend {
#[cfg(all(unix, feature = "wlroots_emulation", not(target_os = "macos")))]
#[serde(rename = "wlroots")]
Wlroots,
#[cfg(all(unix, feature = "libei_emulation", not(target_os = "macos")))]
#[serde(rename = "libei")]
Libei,
#[cfg(all(unix, feature = "rdp_emulation", not(target_os = "macos")))]
#[serde(rename = "xdp")]
Xdp,
#[cfg(all(unix, feature = "x11_emulation", not(target_os = "macos")))]
#[serde(rename = "x11")]
X11,
#[cfg(windows)]
#[serde(rename = "windows")]
Windows,
#[cfg(target_os = "macos")]
#[serde(rename = "macos")]
MacOs,
#[serde(rename = "dummy")]
Dummy,
}
impl From<EmulationBackend> for input_emulation::Backend {
fn from(backend: EmulationBackend) -> Self {
match backend {
#[cfg(all(unix, feature = "wlroots_emulation", not(target_os = "macos")))]
EmulationBackend::Wlroots => Self::Wlroots,
#[cfg(all(unix, feature = "libei_emulation", not(target_os = "macos")))]
EmulationBackend::Libei => Self::Libei,
#[cfg(all(unix, feature = "rdp_emulation", not(target_os = "macos")))]
EmulationBackend::Xdp => Self::Xdp,
#[cfg(all(unix, feature = "x11_emulation", not(target_os = "macos")))]
EmulationBackend::X11 => Self::X11,
#[cfg(windows)]
EmulationBackend::Windows => Self::Windows,
#[cfg(target_os = "macos")]
EmulationBackend::MacOs => Self::MacOs,
EmulationBackend::Dummy => Self::Dummy,
}
}
}
impl Display for EmulationBackend {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
#[cfg(all(unix, feature = "wlroots_emulation", not(target_os = "macos")))]
EmulationBackend::Wlroots => write!(f, "wlroots"),
#[cfg(all(unix, feature = "libei_emulation", not(target_os = "macos")))]
EmulationBackend::Libei => write!(f, "libei"),
#[cfg(all(unix, feature = "rdp_emulation", not(target_os = "macos")))]
EmulationBackend::Xdp => write!(f, "xdg-desktop-portal"),
#[cfg(all(unix, feature = "x11_emulation", not(target_os = "macos")))]
EmulationBackend::X11 => write!(f, "X11"),
#[cfg(windows)]
EmulationBackend::Windows => write!(f, "windows"),
#[cfg(target_os = "macos")]
EmulationBackend::MacOs => write!(f, "macos"),
EmulationBackend::Dummy => write!(f, "dummy"),
}
}
}
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, Serialize, ValueEnum)]
pub enum Frontend {
#[serde(rename = "gtk")]
Gtk,
#[serde(rename = "cli")]
Cli,
}
pub struct Config {
pub frontend: Frontend,
pub port: u16,
pub clients: Vec<(Client, Position)>,
impl Default for Frontend {
fn default() -> Self {
if cfg!(feature = "gtk") {
Self::Gtk
} else {
Self::Cli
}
}
}
#[derive(Debug)]
pub struct Config {
pub capture_backend: Option<CaptureBackend>,
pub emulation_backend: Option<EmulationBackend>,
pub frontend: Frontend,
pub port: u16,
pub clients: Vec<(TomlClient, Position)>,
pub daemon: bool,
pub release_bind: Vec<scancode::Linux>,
pub test_capture: bool,
pub test_emulation: bool,
}
pub struct ConfigClient {
pub ips: HashSet<IpAddr>,
pub hostname: Option<String>,
pub port: u16,
pub pos: Position,
pub active: bool,
pub enter_hook: Option<String>,
}
#[derive(Debug, Error)]
pub enum ConfigError {
#[error(transparent)]
Toml(#[from] toml::de::Error),
#[error(transparent)]
Io(#[from] io::Error),
#[error(transparent)]
Var(#[from] VarError),
}
const DEFAULT_RELEASE_KEYS: [scancode::Linux; 4] =
[KeyLeftCtrl, KeyLeftShift, KeyLeftMeta, KeyLeftAlt];
impl Config {
pub fn new() -> Result<Self, Box<dyn Error>> {
let config_path = "config.toml";
let config_toml = match ConfigToml::new(config_path) {
pub fn new() -> Result<Self, ConfigError> {
let args = CliArgs::parse();
let config_file = "config.toml";
#[cfg(unix)]
let config_path = {
let xdg_config_home =
env::var("XDG_CONFIG_HOME").unwrap_or(format!("{}/.config", env::var("HOME")?));
format!("{xdg_config_home}/lan-mouse/{config_file}")
};
#[cfg(not(unix))]
let config_path = {
let app_data =
env::var("LOCALAPPDATA").unwrap_or(format!("{}/.config", env::var("USERPROFILE")?));
format!("{app_data}\\lan-mouse\\{config_file}")
};
// --config <file> overrules default location
let config_path = args.config.unwrap_or(config_path);
let config_toml = match ConfigToml::new(config_path.as_str()) {
Err(e) => {
log::error!("config.toml: {e}");
log::warn!("{config_path}: {e}");
log::warn!("Continuing without config file ...");
None
},
}
Ok(c) => Some(c),
};
let frontend = match find_arg("--frontend")? {
None => match &config_toml {
Some(c) => c.frontend.clone(),
None => None,
},
frontend => frontend,
};
let frontend_arg = args.frontend;
let frontend_cfg = config_toml.as_ref().and_then(|c| c.frontend);
let frontend = frontend_arg.or(frontend_cfg).unwrap_or_default();
let frontend = match frontend {
None => Frontend::Cli,
Some(s) => match s.as_str() {
"cli" => Frontend::Cli,
"gtk" => Frontend::Gtk,
_ => Frontend::Cli,
}
};
let port = args
.port
.or(config_toml.as_ref().and_then(|c| c.port))
.unwrap_or(DEFAULT_PORT);
let port = match find_arg("--port")? {
Some(port) => port.parse::<u16>()?,
None => match &config_toml {
Some(c) => c.port.unwrap_or(DEFAULT_PORT),
None => DEFAULT_PORT,
}
};
log::debug!("{config_toml:?}");
let release_bind = config_toml
.as_ref()
.and_then(|c| c.release_bind.clone())
.unwrap_or(Vec::from_iter(DEFAULT_RELEASE_KEYS.iter().cloned()));
let mut clients: Vec<(Client, Position)> = vec![];
let capture_backend = args
.capture_backend
.or(config_toml.as_ref().and_then(|c| c.capture_backend));
let emulation_backend = args
.emulation_backend
.or(config_toml.as_ref().and_then(|c| c.emulation_backend));
let mut clients: Vec<(TomlClient, Position)> = vec![];
if let Some(config_toml) = config_toml {
if let Some(c) = config_toml.right {
@@ -129,43 +327,48 @@ impl Config {
}
}
Ok(Config { frontend, clients, port })
let daemon = args.daemon;
let test_capture = args.test_capture;
let test_emulation = args.test_emulation;
Ok(Config {
capture_backend,
emulation_backend,
daemon,
frontend,
clients,
port,
release_bind,
test_capture,
test_emulation,
})
}
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,
pub fn get_clients(&self) -> Vec<ConfigClient> {
self.clients
.iter()
.map(|(c, pos)| {
let port = c.port.unwrap_or(DEFAULT_PORT);
let ips: HashSet<IpAddr> = if let Some(ips) = c.ips.as_ref() {
HashSet::from_iter(ips.iter().cloned())
} else {
HashSet::new()
};
let hostname = match &c.hostname {
Some(h) => Some(h.clone()),
None => c.host_name.clone(),
};
let active = c.activate_on_startup.unwrap_or(false);
let enter_hook = c.enter_hook.clone();
ConfigClient {
ips,
hostname,
port,
pos: *pos,
active,
enter_hook,
}
};
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()
})
.collect()
}
}

View File

@@ -1,99 +0,0 @@
#[cfg(unix)]
use std::env;
use anyhow::Result;
use crate::{backend::consumer, client::{ClientHandle, ClientEvent}, event::Event};
#[cfg(unix)]
#[derive(Debug)]
enum Backend {
Wlroots,
X11,
RemoteDesktopPortal,
Libei,
}
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)]
return Ok(Box::new(consumer::windows::WindowsConsumer::new()));
#[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
}
}
// 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,9 +1,63 @@
use std::{error::Error, net::IpAddr};
use local_channel::mpsc::Receiver;
use std::net::IpAddr;
use trust_dns_resolver::Resolver;
use hickory_resolver::{error::ResolveError, TokioAsyncResolver};
pub fn resolve(host: &str) -> Result<Vec<IpAddr>, Box<dyn Error>> {
log::info!("resolving {host} ...");
let response = Resolver::from_system_conf()?.lookup_ip(host)?;
Ok(response.iter().collect())
use crate::server::Server;
use lan_mouse_ipc::ClientHandle;
pub(crate) struct DnsResolver {
resolver: TokioAsyncResolver,
dns_request: Receiver<ClientHandle>,
}
impl DnsResolver {
pub(crate) fn new(dns_request: Receiver<ClientHandle>) -> Result<Self, ResolveError> {
let resolver = TokioAsyncResolver::tokio_from_system_conf()?;
Ok(Self {
resolver,
dns_request,
})
}
async fn resolve(&self, host: &str) -> Result<Vec<IpAddr>, ResolveError> {
let response = self.resolver.lookup_ip(host).await?;
for ip in response.iter() {
log::info!("{host}: adding ip {ip}");
}
Ok(response.iter().collect())
}
pub(crate) async fn run(mut self, server: Server) {
tokio::select! {
_ = server.cancelled() => {},
_ = self.do_dns(&server) => {},
}
}
async fn do_dns(&mut self, server: &Server) {
loop {
let handle = self.dns_request.recv().await.expect("channel closed");
/* update resolving status */
let hostname = match server.get_hostname(handle) {
Some(hostname) => hostname,
None => continue,
};
log::info!("resolving ({handle}) `{hostname}` ...");
server.set_resolving(handle, true);
let ips = match self.resolve(&hostname).await {
Ok(ips) => ips,
Err(e) => {
log::warn!("could not resolve host '{hostname}': {e}");
vec![]
}
};
server.update_dns_ips(handle, ips);
server.set_resolving(handle, false);
}
}
}

35
src/emulation_test.rs Normal file
View File

@@ -0,0 +1,35 @@
use crate::config::Config;
use input_emulation::{InputEmulation, InputEmulationError};
use input_event::{Event, PointerEvent};
use std::f64::consts::PI;
use std::time::{Duration, Instant};
const FREQUENCY_HZ: f64 = 1.0;
const RADIUS: f64 = 100.0;
pub async fn run(config: Config) -> Result<(), InputEmulationError> {
log::info!("running input emulation test");
let backend = config.emulation_backend.map(|b| b.into());
let mut emulation = InputEmulation::new(backend).await?;
emulation.create(0).await;
let start = Instant::now();
let mut offset = (0, 0);
loop {
tokio::time::sleep(Duration::from_millis(1)).await;
let elapsed = start.elapsed();
let elapsed_sec_f64 = elapsed.as_secs_f64();
let second_fraction = elapsed_sec_f64 - elapsed_sec_f64 as u64 as f64;
let radians = second_fraction * 2. * PI * FREQUENCY_HZ;
let new_offset_f = (radians.cos() * RADIUS * 2., (radians * 2.).sin() * RADIUS);
let new_offset = (new_offset_f.0 as i32, new_offset_f.1 as i32);
if new_offset != offset {
let relative_motion = (new_offset.0 - offset.0, new_offset.1 - offset.1);
offset = new_offset;
let (dx, dy) = (relative_motion.0 as f64, relative_motion.1 as f64);
let event = Event::Pointer(PointerEvent::Motion { time: 0, dx, dy });
emulation.consume(event, 0).await?;
}
}
}

View File

@@ -1,471 +0,0 @@
use std::{error::Error, fmt::{self, Display}};
pub mod server;
#[derive(Debug, Clone, Copy)]
pub enum PointerEvent {
Motion {
time: u32,
relative_x: f64,
relative_y: f64,
},
Button {
time: u32,
button: u32,
state: u32,
},
Axis {
time: u32,
axis: u8,
value: f64,
},
Frame {},
}
#[derive(Debug, Clone, Copy)]
pub enum KeyboardEvent {
Key {
time: u32,
key: u32,
state: u8,
},
Modifiers {
mods_depressed: u32,
mods_latched: u32,
mods_locked: u32,
group: u32,
},
}
#[derive(Debug, Clone, Copy)]
pub enum Event {
Pointer(PointerEvent),
Keyboard(KeyboardEvent),
Release(),
Ping(),
Pong(),
}
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 {
match self {
Self::Pointer(_) => EventType::POINTER,
Self::Keyboard(_) => EventType::KEYBOARD,
Self::Release() => EventType::RELEASE,
Self::Ping() => EventType::PING,
Self::Pong() => EventType::PONG,
}
}
}
impl PointerEvent {
fn event_type(&self) -> PointerEventType {
match self {
Self::Motion { .. } => PointerEventType::MOTION,
Self::Button { .. } => PointerEventType::BUTTON,
Self::Axis { .. } => PointerEventType::AXIS,
Self::Frame { .. } => PointerEventType::FRAME,
}
}
}
impl KeyboardEvent {
fn event_type(&self) -> KeyboardEventType {
match self {
KeyboardEvent::Key { .. } => KeyboardEventType::KEY,
KeyboardEvent::Modifiers { .. } => KeyboardEventType::MODIFIERS,
}
}
}
enum PointerEventType {
MOTION,
BUTTON,
AXIS,
FRAME,
}
enum KeyboardEventType {
KEY,
MODIFIERS,
}
enum EventType {
POINTER,
KEYBOARD,
RELEASE,
PING,
PONG,
}
impl TryFrom<u8> for PointerEventType {
type Error = Box<dyn Error>;
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
x if x == Self::MOTION as u8 => Ok(Self::MOTION),
x if x == Self::BUTTON as u8 => Ok(Self::BUTTON),
x if x == Self::AXIS as u8 => Ok(Self::AXIS),
x if x == Self::FRAME as u8 => Ok(Self::FRAME),
_ => Err(Box::new(ProtocolError {
msg: format!("invalid pointer event type {}", value),
})),
}
}
}
impl TryFrom<u8> for KeyboardEventType {
type Error = Box<dyn Error>;
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
x if x == Self::KEY as u8 => Ok(Self::KEY),
x if x == Self::MODIFIERS as u8 => Ok(Self::MODIFIERS),
_ => Err(Box::new(ProtocolError {
msg: format!("invalid keyboard event type {}", value),
})),
}
}
}
impl Into<Vec<u8>> for &Event {
fn into(self) -> Vec<u8> {
let event_id = vec![self.event_type() as u8];
let event_data = match self {
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()
}
}
#[derive(Debug)]
struct ProtocolError {
msg: String,
}
impl fmt::Display for ProtocolError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Protocol violation: {}", self.msg)
}
}
impl Error for ProtocolError {}
impl TryFrom<Vec<u8>> for Event {
type Error = Box<dyn Error>;
fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
let event_id = u8::from_be_bytes(value[..1].try_into()?);
match event_id {
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),
})),
}
}
}
impl Into<Vec<u8>> for &PointerEvent {
fn into(self) -> Vec<u8> {
let id = vec![self.event_type() as u8];
let data = match self {
PointerEvent::Motion {
time,
relative_x,
relative_y,
} => {
let time = time.to_be_bytes();
let relative_x = relative_x.to_be_bytes();
let relative_y = relative_y.to_be_bytes();
vec![&time[..], &relative_x[..], &relative_y[..]].concat()
}
PointerEvent::Button {
time,
button,
state,
} => {
let time = time.to_be_bytes();
let button = button.to_be_bytes();
let state = state.to_be_bytes();
vec![&time[..], &button[..], &state[..]].concat()
}
PointerEvent::Axis { time, axis, value } => {
let time = time.to_be_bytes();
let axis = axis.to_be_bytes();
let value = value.to_be_bytes();
vec![&time[..], &axis[..], &value[..]].concat()
}
PointerEvent::Frame {} => {
vec![]
}
};
vec![id, data].concat()
}
}
impl TryFrom<Vec<u8>> for PointerEvent {
type Error = Box<dyn Error>;
fn try_from(data: Vec<u8>) -> Result<Self, Self::Error> {
match data.get(1) {
Some(id) => {
let event_type = match id.to_owned().try_into() {
Ok(event_type) => event_type,
Err(e) => return Err(e),
};
match event_type {
PointerEventType::MOTION => {
let time = match data.get(2..6) {
Some(d) => u32::from_be_bytes(d.try_into()?),
None => {
return Err(Box::new(ProtocolError {
msg: "Expected 4 Bytes at index 2".into(),
}))
}
};
let relative_x = match data.get(6..14) {
Some(d) => f64::from_be_bytes(d.try_into()?),
None => {
return Err(Box::new(ProtocolError {
msg: "Expected 8 Bytes at index 6".into(),
}))
}
};
let relative_y = match data.get(14..22) {
Some(d) => f64::from_be_bytes(d.try_into()?),
None => {
return Err(Box::new(ProtocolError {
msg: "Expected 8 Bytes at index 14".into(),
}))
}
};
Ok(Self::Motion {
time,
relative_x,
relative_y,
})
}
PointerEventType::BUTTON => {
let time = match data.get(2..6) {
Some(d) => u32::from_be_bytes(d.try_into()?),
None => {
return Err(Box::new(ProtocolError {
msg: "Expected 4 Bytes at index 2".into(),
}))
}
};
let button = match data.get(6..10) {
Some(d) => u32::from_be_bytes(d.try_into()?),
None => {
return Err(Box::new(ProtocolError {
msg: "Expected 4 Bytes at index 10".into(),
}))
}
};
let state = match data.get(10..14) {
Some(d) => u32::from_be_bytes(d.try_into()?),
None => {
return Err(Box::new(ProtocolError {
msg: "Expected 4 Bytes at index 14".into(),
}))
}
};
Ok(Self::Button {
time,
button,
state,
})
}
PointerEventType::AXIS => {
let time = match data.get(2..6) {
Some(d) => u32::from_be_bytes(d.try_into()?),
None => {
return Err(Box::new(ProtocolError {
msg: "Expected 4 Bytes at index 2".into(),
}))
}
};
let axis = match data.get(6) {
Some(d) => *d,
None => {
return Err(Box::new(ProtocolError {
msg: "Expected 1 Byte at index 6".into(),
}));
}
};
let value = match data.get(7..15) {
Some(d) => f64::from_be_bytes(d.try_into()?),
None => {
return Err(Box::new(ProtocolError {
msg: "Expected 8 Bytes at index 7".into(),
}));
}
};
Ok(Self::Axis { time, axis, value })
}
PointerEventType::FRAME => Ok(Self::Frame {}),
}
}
None => Err(Box::new(ProtocolError {
msg: "Expected an element at index 0".into(),
})),
}
}
}
impl Into<Vec<u8>> for &KeyboardEvent {
fn into(self) -> Vec<u8> {
let id = vec![self.event_type() as u8];
let data = match self {
KeyboardEvent::Key { time, key, state } => {
let time = time.to_be_bytes();
let key = key.to_be_bytes();
let state = state.to_be_bytes();
vec![&time[..], &key[..], &state[..]].concat()
}
KeyboardEvent::Modifiers {
mods_depressed,
mods_latched,
mods_locked,
group,
} => {
let mods_depressed = mods_depressed.to_be_bytes();
let mods_latched = mods_latched.to_be_bytes();
let mods_locked = mods_locked.to_be_bytes();
let group = group.to_be_bytes();
vec![
&mods_depressed[..],
&mods_latched[..],
&mods_locked[..],
&group[..],
]
.concat()
}
};
vec![id, data].concat()
}
}
impl TryFrom<Vec<u8>> for KeyboardEvent {
type Error = Box<dyn Error>;
fn try_from(data: Vec<u8>) -> Result<Self, Self::Error> {
match data.get(1) {
Some(id) => {
let event_type = match id.to_owned().try_into() {
Ok(event_type) => event_type,
Err(e) => return Err(e),
};
match event_type {
KeyboardEventType::KEY => {
let time = match data.get(2..6) {
Some(d) => u32::from_be_bytes(d.try_into()?),
None => {
return Err(Box::new(ProtocolError {
msg: "Expected 4 Bytes at index 6".into(),
}))
}
};
let key = match data.get(6..10) {
Some(d) => u32::from_be_bytes(d.try_into()?),
None => {
return Err(Box::new(ProtocolError {
msg: "Expected 4 Bytes at index 10".into(),
}))
}
};
let state = match data.get(10) {
Some(d) => *d,
None => {
return Err(Box::new(ProtocolError {
msg: "Expected 1 Bytes at index 14".into(),
}))
}
};
Ok(KeyboardEvent::Key { time, key, state })
}
KeyboardEventType::MODIFIERS => {
let mods_depressed = match data.get(2..6) {
Some(d) => u32::from_be_bytes(d.try_into()?),
None => {
return Err(Box::new(ProtocolError {
msg: "Expected 4 Bytes at index 6".into(),
}))
}
};
let mods_latched = match data.get(6..10) {
Some(d) => u32::from_be_bytes(d.try_into()?),
None => {
return Err(Box::new(ProtocolError {
msg: "Expected 4 Bytes at index 10".into(),
}))
}
};
let mods_locked = match data.get(10..14) {
Some(d) => u32::from_be_bytes(d.try_into()?),
None => {
return Err(Box::new(ProtocolError {
msg: "Expected 4 Bytes at index 14".into(),
}))
}
};
let group = match data.get(14..18) {
Some(d) => u32::from_be_bytes(d.try_into()?),
None => {
return Err(Box::new(ProtocolError {
msg: "Expected 4 Bytes at index 18".into(),
}))
}
};
Ok(KeyboardEvent::Modifiers {
mods_depressed,
mods_latched,
mods_locked,
group,
})
}
}
}
None => Err(Box::new(ProtocolError {
msg: "Expected an element at index 0".into(),
})),
}
}
}

View File

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

View File

@@ -1,115 +0,0 @@
use std::io::{Read, Result};
use std::{str, net::SocketAddr};
#[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 {
RequestPortChange(u16),
RequestClientAdd(SocketAddr, Position),
RequestClientDelete(Client),
RequestClientUpdate(Client),
RequestShutdown(),
}
#[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)]
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
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();
}
}
pub trait Frontend { }

View File

@@ -1,85 +0,0 @@
use anyhow::Result;
use std::{thread, io::Write, net::SocketAddr};
#[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;
use super::{FrontendEvent, Frontend};
pub struct CliFrontend;
impl Frontend for CliFrontend {}
impl CliFrontend {
pub fn new() -> Result<CliFrontend> {
#[cfg(unix)]
let socket_path = Path::new(env::var("XDG_RUNTIME_DIR")?.as_str()).join("lan-mouse-socket.sock");
thread::Builder::new()
.name("cli-frontend".to_string())
.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_event(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::RequestShutdown() {
break;
}
}
}
Err(e) => {
log::error!("{e:?}");
break
}
}
}
}).unwrap();
Ok(Self {})
}
}
fn parse_event(s: String, len: usize) -> Option<FrontendEvent> {
if len == 0 {
return Some(FrontendEvent::RequestShutdown())
}
let mut l = s.split_whitespace();
let cmd = l.next()?;
match cmd {
"connect" => {
let addr = match l.next()?.parse() {
Ok(addr) => SocketAddr::V4(addr),
Err(e) => {
log::error!("parse error: {e}");
return None;
}
};
Some(FrontendEvent::RequestClientAdd(addr, Position::Left ))
}
_ => {
log::error!("unknown command: {s}");
None
}
}
}

View File

@@ -1,49 +0,0 @@
use std::io::{self, Write};
use crate::client::Position;
pub fn ask_confirmation(default: bool) -> Result<bool, io::Error> {
eprint!("{}", if default {" [Y,n] "} else { " [y,N] "});
io::stderr().flush()?;
let answer = loop {
let mut buffer = String::new();
io::stdin().read_line(&mut buffer)?;
let answer = buffer.to_lowercase();
let answer = answer.trim();
match answer {
"" => break default,
"y" => break true,
"n" => break false,
_ => {
eprint!("Enter y for Yes or n for No: ");
io::stderr().flush()?;
continue
}
}
};
Ok(answer)
}
pub fn ask_position() -> Result<Position, io::Error> {
eprint!("Enter position - top (t) | bottom (b) | left(l) | right(r): ");
io::stderr().flush()?;
let pos = loop {
let mut buffer = String::new();
io::stdin().read_line(&mut buffer)?;
let answer = buffer.to_lowercase();
let answer = answer.trim();
match answer {
"t" | "top" => break Position::Top,
"b" | "bottom" => break Position::Bottom,
"l" | "left" => break Position::Right,
"r" | "right" => break Position::Left,
_ => {
eprint!("Invalid position: {answer} - enter top (t) | bottom (b) | left(l) | right(r): ");
io::stderr().flush()?;
continue
}
};
};
Ok(pos)
}

View File

@@ -1,11 +1,7 @@
pub mod client;
pub mod config;
pub mod dns;
pub mod event;
pub mod server;
pub mod consumer;
pub mod producer;
pub mod backend;
pub mod frontend;
pub mod ioutils;
pub mod capture_test;
pub mod emulation_test;

View File

@@ -1,14 +1,38 @@
use std::{process, error::Error};
use env_logger::Env;
use input_capture::InputCaptureError;
use input_emulation::InputEmulationError;
use lan_mouse::{
consumer, producer,
config::{Config, Frontend::{Gtk, Cli}}, event::server::Server,
frontend::{FrontendAdapter, cli::CliFrontend},
capture_test,
config::{Config, ConfigError, Frontend},
emulation_test,
server::{Server, ServiceError},
};
use lan_mouse_ipc::IpcError;
use std::{
future::Future,
io,
process::{self, Child, Command},
};
use thiserror::Error;
use tokio::task::LocalSet;
#[derive(Debug, Error)]
enum LanMouseError {
#[error(transparent)]
Service(#[from] ServiceError),
#[error(transparent)]
IpcError(#[from] IpcError),
#[error(transparent)]
Config(#[from] ConfigError),
#[error(transparent)]
Io(#[from] io::Error),
#[error(transparent)]
Capture(#[from] InputCaptureError),
#[error(transparent)]
Emulation(#[from] InputEmulationError),
}
pub fn main() {
// init logging
let env = Env::default().filter_or("LAN_MOUSE_LOG_LEVEL", "info");
env_logger::init_from_env(env);
@@ -19,47 +43,80 @@ pub fn main() {
}
}
pub fn run() -> Result<(), Box<dyn Error>> {
// parse config file
fn run() -> Result<(), LanMouseError> {
// parse config file + cli args
let config = Config::new()?;
log::debug!("{config:?}");
log::info!("release bind: {:?}", config.release_bind);
// 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)?;
// add clients form 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);
if config.test_capture {
run_async(capture_test::run(config))?;
} else if config.test_emulation {
run_async(emulation_test::run(config))?;
} else if config.daemon {
// if daemon is specified we run the service
run_async(run_service(config))?;
} else {
// otherwise start the service as a child process and
// run a frontend
let mut service = start_service()?;
run_frontend(&config)?;
#[cfg(unix)]
{
// on unix we give the service a chance to terminate gracefully
let pid = service.id() as libc::pid_t;
unsafe {
libc::kill(pid, libc::SIGINT);
}
service.wait()?;
}
log::info!("adding client [{}]{} @ {:?}", p, host_name, c);
event_server.add_client(c, p);
});
// any threads need to be started after event_server sets up signal handling
match config.frontend {
Gtk => {
#[cfg(all(unix, feature = "gtk"))]
frontend::gtk::create();
#[cfg(not(feature = "gtk"))]
panic!("gtk frontend requested but feature not enabled!");
},
Cli => Box::new(CliFrontend::new()?),
};
log::info!("Press Ctrl+Alt+Shift+Super to release the mouse");
// run event loop
event_server.run()?;
service.kill()?;
}
Ok(())
}
fn run_async<F, E>(f: F) -> Result<(), LanMouseError>
where
F: Future<Output = Result<(), E>>,
LanMouseError: From<E>,
{
// create single threaded tokio runtime
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_io()
.enable_time()
.build()?;
// run async event loop
Ok(runtime.block_on(LocalSet::new().run_until(f))?)
}
fn start_service() -> Result<Child, io::Error> {
let child = Command::new(std::env::current_exe()?)
.args(std::env::args().skip(1))
.arg("--daemon")
.spawn()?;
Ok(child)
}
async fn run_service(config: Config) -> Result<(), ServiceError> {
log::info!("Press {:?} to release the mouse", config.release_bind);
Server::new(config).run().await?;
log::info!("service exited!");
Ok(())
}
fn run_frontend(config: &Config) -> Result<(), IpcError> {
match config.frontend {
#[cfg(feature = "gtk")]
Frontend::Gtk => {
lan_mouse_gtk::run();
}
#[cfg(not(feature = "gtk"))]
Frontend::Gtk => panic!("gtk frontend requested but feature not enabled!"),
Frontend::Cli => {
lan_mouse_cli::run()?;
}
};
Ok(())
}

View File

@@ -1,63 +0,0 @@
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;
#[cfg(unix)]
enum Backend {
Wayland,
X11,
}
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" => {
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);
}

562
src/server.rs Normal file
View File

@@ -0,0 +1,562 @@
use capture_task::CaptureRequest;
use emulation_task::EmulationRequest;
use futures::StreamExt;
use hickory_resolver::error::ResolveError;
use local_channel::mpsc::{channel, Sender};
use log;
use std::{
cell::{Cell, RefCell},
collections::{HashSet, VecDeque},
io,
net::{IpAddr, SocketAddr},
rc::Rc,
};
use thiserror::Error;
use tokio::{join, signal, sync::Notify};
use tokio_util::sync::CancellationToken;
use crate::{client::ClientManager, config::Config, dns::DnsResolver};
use lan_mouse_ipc::{
AsyncFrontendListener, ClientConfig, ClientHandle, ClientState, FrontendEvent, FrontendRequest,
ListenerCreationError, Position, Status,
};
mod capture_task;
mod emulation_task;
mod network_task;
mod ping_task;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum State {
/// Currently sending events to another device
Sending,
/// Currently receiving events from other devices
Receiving,
/// Entered the deadzone of another device but waiting
/// for acknowledgement (Leave event) from the device
AwaitAck,
}
#[derive(Debug, Error)]
pub enum ServiceError {
#[error(transparent)]
Dns(#[from] ResolveError),
#[error(transparent)]
Listen(#[from] ListenerCreationError),
#[error(transparent)]
Io(#[from] io::Error),
}
#[derive(Clone)]
pub struct Server {
active_client: Rc<Cell<Option<ClientHandle>>>,
pub(crate) client_manager: Rc<RefCell<ClientManager>>,
port: Rc<Cell<u16>>,
state: Rc<Cell<State>>,
release_bind: Vec<input_event::scancode::Linux>,
notifies: Rc<Notifies>,
config: Rc<Config>,
pending_frontend_events: Rc<RefCell<VecDeque<FrontendEvent>>>,
capture_status: Rc<Cell<Status>>,
emulation_status: Rc<Cell<Status>>,
}
#[derive(Default)]
struct Notifies {
capture: Notify,
emulation: Notify,
ping: Notify,
port_changed: Notify,
frontend_event_pending: Notify,
cancel: CancellationToken,
}
impl Server {
pub fn new(config: Config) -> Self {
let active_client = Rc::new(Cell::new(None));
let client_manager = Rc::new(RefCell::new(ClientManager::default()));
let state = Rc::new(Cell::new(State::Receiving));
let port = Rc::new(Cell::new(config.port));
for config_client in config.get_clients() {
let client = ClientConfig {
hostname: config_client.hostname,
fix_ips: config_client.ips.into_iter().collect(),
port: config_client.port,
pos: config_client.pos,
cmd: config_client.enter_hook,
};
let state = ClientState {
active: config_client.active,
ips: HashSet::from_iter(client.fix_ips.iter().cloned()),
..Default::default()
};
let mut client_manager = client_manager.borrow_mut();
let handle = client_manager.add_client();
let c = client_manager.get_mut(handle).expect("invalid handle");
*c = (client, state);
}
// task notification tokens
let notifies = Rc::new(Notifies::default());
let release_bind = config.release_bind.clone();
let config = Rc::new(config);
Self {
config,
active_client,
client_manager,
port,
state,
release_bind,
notifies,
pending_frontend_events: Rc::new(RefCell::new(VecDeque::new())),
capture_status: Default::default(),
emulation_status: Default::default(),
}
}
pub async fn run(&mut self) -> Result<(), ServiceError> {
// create frontend communication adapter, exit if already running
let mut frontend = match AsyncFrontendListener::new().await {
Ok(f) => f,
Err(ListenerCreationError::AlreadyRunning) => {
log::info!("service already running, exiting");
return Ok(());
}
e => e?,
};
let (capture_tx, capture_rx) = channel(); /* requests for input capture */
let (emulation_tx, emulation_rx) = channel(); /* emulation requests */
let (udp_recv_tx, udp_recv_rx) = channel(); /* udp receiver */
let (udp_send_tx, udp_send_rx) = channel(); /* udp sender */
let (dns_tx, dns_rx) = channel(); /* dns requests */
let network = network_task::new(self.clone(), udp_recv_tx.clone(), udp_send_rx).await?;
let capture = capture_task::new(self.clone(), capture_rx, udp_send_tx.clone());
let emulation =
emulation_task::new(self.clone(), emulation_rx, udp_recv_rx, udp_send_tx.clone());
let resolver = DnsResolver::new(dns_rx)?;
let dns_task = tokio::task::spawn_local(resolver.run(self.clone()));
// task that pings clients to see if they are responding
let ping = ping_task::new(
self.clone(),
udp_send_tx.clone(),
emulation_tx.clone(),
capture_tx.clone(),
);
for handle in self.active_clients() {
dns_tx.send(handle).expect("channel closed");
}
loop {
tokio::select! {
request = frontend.next() => {
let request = match request {
Some(Ok(r)) => r,
Some(Err(e)) => {
log::error!("error receiving request: {e}");
continue;
}
None => break,
};
log::debug!("handle frontend request: {request:?}");
self.handle_request(&capture_tx.clone(), &emulation_tx.clone(), request, &dns_tx);
}
_ = self.notifies.frontend_event_pending.notified() => {
while let Some(event) = {
/* need to drop borrow before next iteration! */
let event = self.pending_frontend_events.borrow_mut().pop_front();
event
} {
frontend.broadcast(event).await;
}
},
_ = self.cancelled() => break,
r = signal::ctrl_c() => {
r.expect("failed to wait for CTRL+C");
break;
}
}
}
log::info!("terminating service");
self.cancel();
let _ = join!(capture, dns_task, emulation, network, ping);
Ok(())
}
fn notify_frontend(&self, event: FrontendEvent) {
self.pending_frontend_events.borrow_mut().push_back(event);
self.notifies.frontend_event_pending.notify_one();
}
fn cancel(&self) {
self.notifies.cancel.cancel();
}
pub(crate) async fn cancelled(&self) {
self.notifies.cancel.cancelled().await
}
fn is_cancelled(&self) -> bool {
self.notifies.cancel.is_cancelled()
}
fn notify_capture(&self) {
log::info!("received capture enable request");
self.notifies.capture.notify_waiters()
}
async fn capture_enabled(&self) {
self.notifies.capture.notified().await
}
fn notify_emulation(&self) {
log::info!("received emulation enable request");
self.notifies.emulation.notify_waiters()
}
async fn emulation_notified(&self) {
self.notifies.emulation.notified().await
}
fn restart_ping_timer(&self) {
self.notifies.ping.notify_waiters()
}
async fn ping_timer_notified(&self) {
self.notifies.ping.notified().await
}
fn request_port_change(&self, port: u16) {
self.port.replace(port);
self.notifies.port_changed.notify_one();
}
fn notify_port_changed(&self, port: u16, msg: Option<String>) {
self.port.replace(port);
self.notify_frontend(FrontendEvent::PortChanged(port, msg));
}
pub(crate) fn client_updated(&self, handle: ClientHandle) {
self.notify_frontend(FrontendEvent::Changed(handle));
}
fn active_clients(&self) -> Vec<ClientHandle> {
self.client_manager
.borrow()
.get_client_states()
.filter(|(_, (_, s))| s.active)
.map(|(h, _)| h)
.collect()
}
fn handle_request(
&self,
capture: &Sender<CaptureRequest>,
emulate: &Sender<EmulationRequest>,
event: FrontendRequest,
dns: &Sender<ClientHandle>,
) -> bool {
log::debug!("frontend: {event:?}");
match event {
FrontendRequest::EnableCapture => self.notify_capture(),
FrontendRequest::EnableEmulation => self.notify_emulation(),
FrontendRequest::Create => {
self.add_client();
}
FrontendRequest::Activate(handle, active) => {
if active {
self.activate_client(capture, emulate, handle);
} else {
self.deactivate_client(capture, emulate, handle);
}
}
FrontendRequest::ChangePort(port) => self.request_port_change(port),
FrontendRequest::Delete(handle) => {
self.remove_client(capture, emulate, handle);
self.notify_frontend(FrontendEvent::Deleted(handle));
}
FrontendRequest::Enumerate() => self.enumerate(),
FrontendRequest::GetState(handle) => self.broadcast_client(handle),
FrontendRequest::UpdateFixIps(handle, fix_ips) => self.update_fix_ips(handle, fix_ips),
FrontendRequest::UpdateHostname(handle, host) => {
self.update_hostname(handle, host, dns)
}
FrontendRequest::UpdatePort(handle, port) => self.update_port(handle, port),
FrontendRequest::UpdatePosition(handle, pos) => {
self.update_pos(handle, capture, emulate, pos)
}
FrontendRequest::ResolveDns(handle) => dns.send(handle).expect("channel closed"),
FrontendRequest::Sync => {
self.enumerate();
self.notify_frontend(FrontendEvent::EmulationStatus(self.emulation_status.get()));
self.notify_frontend(FrontendEvent::CaptureStatus(self.capture_status.get()));
self.notify_frontend(FrontendEvent::PortChanged(self.port.get(), None));
}
};
false
}
fn enumerate(&self) {
let clients = self
.client_manager
.borrow()
.get_client_states()
.map(|(h, (c, s))| (h, c.clone(), s.clone()))
.collect();
self.notify_frontend(FrontendEvent::Enumerate(clients));
}
fn add_client(&self) -> ClientHandle {
let handle = self.client_manager.borrow_mut().add_client();
log::info!("added client {handle}");
let (c, s) = self.client_manager.borrow().get(handle).unwrap().clone();
self.notify_frontend(FrontendEvent::Created(handle, c, s));
handle
}
fn deactivate_client(
&self,
capture: &Sender<CaptureRequest>,
emulate: &Sender<EmulationRequest>,
handle: ClientHandle,
) {
match self.client_manager.borrow_mut().get_mut(handle) {
None => return,
Some((_, s)) if !s.active => return,
Some((_, s)) => s.active = false,
};
let _ = capture.send(CaptureRequest::Destroy(handle));
let _ = emulate.send(EmulationRequest::Destroy(handle));
self.client_updated(handle);
log::info!("deactivated client {handle}");
}
fn activate_client(
&self,
capture: &Sender<CaptureRequest>,
emulate: &Sender<EmulationRequest>,
handle: ClientHandle,
) {
/* deactivate potential other client at this position */
let pos = match self.client_manager.borrow().get(handle) {
None => return,
Some((_, s)) if s.active => return,
Some((client, _)) => client.pos,
};
let other = self.client_manager.borrow_mut().find_client(pos);
if let Some(other) = other {
self.deactivate_client(capture, emulate, other);
}
/* activate the client */
if let Some((_, s)) = self.client_manager.borrow_mut().get_mut(handle) {
s.active = true;
} else {
return;
};
/* notify emulation, capture and frontends */
let _ = capture.send(CaptureRequest::Create(handle, to_capture_pos(pos)));
let _ = emulate.send(EmulationRequest::Create(handle));
self.client_updated(handle);
log::info!("activated client {handle} ({pos})");
}
fn remove_client(
&self,
capture: &Sender<CaptureRequest>,
emulate: &Sender<EmulationRequest>,
handle: ClientHandle,
) {
let Some(active) = self
.client_manager
.borrow_mut()
.remove_client(handle)
.map(|(_, s)| s.active)
else {
return;
};
if active {
let _ = capture.send(CaptureRequest::Destroy(handle));
let _ = emulate.send(EmulationRequest::Destroy(handle));
}
}
fn update_pressed_keys(&self, handle: ClientHandle, has_pressed_keys: bool) {
if let Some((_, s)) = self.client_manager.borrow_mut().get_mut(handle) {
s.has_pressed_keys = has_pressed_keys;
}
}
fn update_fix_ips(&self, handle: ClientHandle, fix_ips: Vec<IpAddr>) {
if let Some((c, _)) = self.client_manager.borrow_mut().get_mut(handle) {
c.fix_ips = fix_ips;
};
self.update_ips(handle);
self.client_updated(handle);
}
pub(crate) fn update_dns_ips(&self, handle: ClientHandle, dns_ips: Vec<IpAddr>) {
if let Some((_, s)) = self.client_manager.borrow_mut().get_mut(handle) {
s.dns_ips = dns_ips;
};
self.update_ips(handle);
self.client_updated(handle);
}
fn update_ips(&self, handle: ClientHandle) {
if let Some((c, s)) = self.client_manager.borrow_mut().get_mut(handle) {
s.ips = c
.fix_ips
.iter()
.cloned()
.chain(s.dns_ips.iter().cloned())
.collect::<HashSet<_>>();
}
}
fn update_hostname(
&self,
handle: ClientHandle,
hostname: Option<String>,
dns: &Sender<ClientHandle>,
) {
let mut client_manager = self.client_manager.borrow_mut();
let Some((c, s)) = client_manager.get_mut(handle) else {
return;
};
// hostname changed
if c.hostname != hostname {
c.hostname = hostname;
s.active_addr = None;
s.dns_ips.clear();
drop(client_manager);
self.update_ips(handle);
dns.send(handle).expect("channel closed");
}
self.client_updated(handle);
}
fn update_port(&self, handle: ClientHandle, port: u16) {
let mut client_manager = self.client_manager.borrow_mut();
let Some((c, s)) = client_manager.get_mut(handle) else {
return;
};
if c.port != port {
c.port = port;
s.active_addr = s.active_addr.map(|a| SocketAddr::new(a.ip(), port));
}
}
fn update_pos(
&self,
handle: ClientHandle,
capture: &Sender<CaptureRequest>,
emulate: &Sender<EmulationRequest>,
pos: Position,
) {
let (changed, active) = {
let mut client_manager = self.client_manager.borrow_mut();
let Some((c, s)) = client_manager.get_mut(handle) else {
return;
};
let changed = c.pos != pos;
if changed {
log::info!("update pos {handle} {} -> {}", c.pos, pos);
}
c.pos = pos;
(changed, s.active)
};
// update state in event input emulator & input capture
if changed {
self.deactivate_client(capture, emulate, handle);
if active {
self.activate_client(capture, emulate, handle);
}
}
}
fn broadcast_client(&self, handle: ClientHandle) {
let client = self.client_manager.borrow().get(handle).cloned();
let event = if let Some((config, state)) = client {
FrontendEvent::State(handle, config, state)
} else {
FrontendEvent::NoSuchClient(handle)
};
self.notify_frontend(event);
}
fn set_emulation_status(&self, status: Status) {
self.emulation_status.replace(status);
let status = FrontendEvent::EmulationStatus(status);
self.notify_frontend(status);
}
fn set_capture_status(&self, status: Status) {
self.capture_status.replace(status);
let status = FrontendEvent::CaptureStatus(status);
self.notify_frontend(status);
}
pub(crate) fn set_resolving(&self, handle: ClientHandle, status: bool) {
if let Some((_, s)) = self.client_manager.borrow_mut().get_mut(handle) {
s.resolving = status;
}
self.client_updated(handle);
}
pub(crate) fn get_hostname(&self, handle: ClientHandle) -> Option<String> {
self.client_manager
.borrow_mut()
.get_mut(handle)
.and_then(|(c, _)| c.hostname.clone())
}
fn get_state(&self) -> State {
self.state.get()
}
fn set_state(&self, state: State) {
log::debug!("state => {state:?}");
self.state.replace(state);
}
fn set_active(&self, handle: Option<ClientHandle>) {
log::debug!("active client => {handle:?}");
self.active_client.replace(handle);
}
fn active_addr(&self, handle: ClientHandle) -> Option<SocketAddr> {
self.client_manager
.borrow()
.get(handle)
.and_then(|(_, s)| s.active_addr)
}
}
fn to_capture_pos(pos: Position) -> input_capture::Position {
match pos {
Position::Left => input_capture::Position::Left,
Position::Right => input_capture::Position::Right,
Position::Top => input_capture::Position::Top,
Position::Bottom => input_capture::Position::Bottom,
}
}

206
src/server/capture_task.rs Normal file
View File

@@ -0,0 +1,206 @@
use futures::StreamExt;
use lan_mouse_proto::ProtoEvent;
use local_channel::mpsc::{Receiver, Sender};
use std::net::SocketAddr;
use tokio::{process::Command, task::JoinHandle};
use input_capture::{
self, CaptureError, CaptureEvent, CaptureHandle, InputCapture, InputCaptureError, Position,
};
use crate::server::State;
use lan_mouse_ipc::{ClientHandle, Status};
use super::Server;
#[derive(Clone, Copy, Debug)]
pub(crate) enum CaptureRequest {
/// capture must release the mouse
Release,
/// add a capture client
Create(CaptureHandle, Position),
/// destory a capture client
Destroy(CaptureHandle),
}
pub(crate) fn new(
server: Server,
capture_rx: Receiver<CaptureRequest>,
udp_send: Sender<(ProtoEvent, SocketAddr)>,
) -> JoinHandle<()> {
let backend = server.config.capture_backend.map(|b| b.into());
tokio::task::spawn_local(capture_task(server, backend, udp_send, capture_rx))
}
async fn capture_task(
server: Server,
backend: Option<input_capture::Backend>,
sender_tx: Sender<(ProtoEvent, SocketAddr)>,
mut notify_rx: Receiver<CaptureRequest>,
) {
loop {
if let Err(e) = do_capture(backend, &server, &sender_tx, &mut notify_rx).await {
log::warn!("input capture exited: {e}");
}
server.set_capture_status(Status::Disabled);
if server.is_cancelled() {
break;
}
// allow cancellation
loop {
tokio::select! {
_ = notify_rx.recv() => continue, /* need to ignore requests here! */
_ = server.capture_enabled() => break,
_ = server.cancelled() => return,
}
}
}
}
async fn do_capture(
backend: Option<input_capture::Backend>,
server: &Server,
sender_tx: &Sender<(ProtoEvent, SocketAddr)>,
notify_rx: &mut Receiver<CaptureRequest>,
) -> Result<(), InputCaptureError> {
/* allow cancelling capture request */
let mut capture = tokio::select! {
r = InputCapture::new(backend) => r?,
_ = server.cancelled() => return Ok(()),
};
server.set_capture_status(Status::Enabled);
let clients = server.active_clients();
let clients = clients.iter().copied().map(|handle| {
(
handle,
server
.client_manager
.borrow()
.get(handle)
.map(|(c, _)| c.pos)
.expect("no such client"),
)
});
for (handle, pos) in clients {
capture.create(handle, to_capture_pos(pos)).await?;
}
loop {
tokio::select! {
event = capture.next() => match event {
Some(event) => handle_capture_event(server, &mut capture, sender_tx, event?).await?,
None => return Ok(()),
},
e = notify_rx.recv() => {
log::debug!("input capture notify rx: {e:?}");
match e {
Some(e) => match e {
CaptureRequest::Release => {
capture.release().await?;
server.state.replace(State::Receiving);
}
CaptureRequest::Create(h, p) => capture.create(h, p).await?,
CaptureRequest::Destroy(h) => capture.destroy(h).await?,
},
None => break,
}
}
_ = server.cancelled() => break,
}
}
capture.terminate().await?;
Ok(())
}
async fn handle_capture_event(
server: &Server,
capture: &mut InputCapture,
sender_tx: &Sender<(ProtoEvent, SocketAddr)>,
event: (CaptureHandle, CaptureEvent),
) -> Result<(), CaptureError> {
let (handle, event) = event;
log::trace!("({handle}) {event:?}");
// capture started
if event == CaptureEvent::Begin {
// wait for remote to acknowlegde enter
server.set_state(State::AwaitAck);
server.set_active(Some(handle));
// restart ping timer to release capture if unreachable
server.restart_ping_timer();
// spawn enter hook cmd
spawn_hook_command(server, handle);
}
// release capture if emulation set state to Receiveing
if server.get_state() == State::Receiving {
capture.release().await?;
return Ok(());
}
// check release bind
if capture.keys_pressed(&server.release_bind) {
capture.release().await?;
server.set_state(State::Receiving);
}
if let Some(addr) = server.active_addr(handle) {
let event = match server.get_state() {
State::Sending => match event {
CaptureEvent::Begin => ProtoEvent::Enter(0),
CaptureEvent::Input(e) => ProtoEvent::Input(e),
},
/* send additional enter events until acknowleged */
State::AwaitAck => ProtoEvent::Enter(0),
/* released capture */
State::Receiving => ProtoEvent::Leave(0),
};
sender_tx.send((event, addr)).expect("sender closed");
};
Ok(())
}
fn spawn_hook_command(server: &Server, handle: ClientHandle) {
let Some(cmd) = server
.client_manager
.borrow()
.get(handle)
.and_then(|(c, _)| c.cmd.clone())
else {
return;
};
tokio::task::spawn_local(async move {
log::info!("spawning command!");
let mut child = match Command::new("sh").arg("-c").arg(cmd.as_str()).spawn() {
Ok(c) => c,
Err(e) => {
log::warn!("could not execute cmd: {e}");
return;
}
};
match child.wait().await {
Ok(s) => {
if s.success() {
log::info!("{cmd} exited successfully");
} else {
log::warn!("{cmd} exited with {s}");
}
}
Err(e) => log::warn!("{cmd}: {e}"),
}
});
}
fn to_capture_pos(pos: lan_mouse_ipc::Position) -> input_capture::Position {
match pos {
lan_mouse_ipc::Position::Left => input_capture::Position::Left,
lan_mouse_ipc::Position::Right => input_capture::Position::Right,
lan_mouse_ipc::Position::Top => input_capture::Position::Top,
lan_mouse_ipc::Position::Bottom => input_capture::Position::Bottom,
}
}

View File

@@ -0,0 +1,188 @@
use local_channel::mpsc::{Receiver, Sender};
use std::net::SocketAddr;
use lan_mouse_proto::ProtoEvent;
use tokio::task::JoinHandle;
use lan_mouse_ipc::ClientHandle;
use crate::{client::ClientManager, server::State};
use input_emulation::{self, EmulationError, EmulationHandle, InputEmulation, InputEmulationError};
use lan_mouse_ipc::Status;
use super::{network_task::NetworkError, Server};
#[derive(Clone, Debug)]
pub(crate) enum EmulationRequest {
/// create a new client
Create(EmulationHandle),
/// destroy a client
Destroy(EmulationHandle),
/// input emulation must release keys for client
ReleaseKeys(ClientHandle),
}
pub(crate) fn new(
server: Server,
emulation_rx: Receiver<EmulationRequest>,
udp_rx: Receiver<Result<(ProtoEvent, SocketAddr), NetworkError>>,
sender_tx: Sender<(ProtoEvent, SocketAddr)>,
) -> JoinHandle<()> {
let emulation_task = emulation_task(server, emulation_rx, udp_rx, sender_tx);
tokio::task::spawn_local(emulation_task)
}
async fn emulation_task(
server: Server,
mut rx: Receiver<EmulationRequest>,
mut udp_rx: Receiver<Result<(ProtoEvent, SocketAddr), NetworkError>>,
sender_tx: Sender<(ProtoEvent, SocketAddr)>,
) {
loop {
if let Err(e) = do_emulation(&server, &mut rx, &mut udp_rx, &sender_tx).await {
log::warn!("input emulation exited: {e}");
}
server.set_emulation_status(Status::Disabled);
if server.is_cancelled() {
break;
}
// allow cancellation
loop {
tokio::select! {
_ = rx.recv() => continue, /* need to ignore requests here! */
_ = server.emulation_notified() => break,
_ = server.cancelled() => return,
}
}
}
}
async fn do_emulation(
server: &Server,
rx: &mut Receiver<EmulationRequest>,
udp_rx: &mut Receiver<Result<(ProtoEvent, SocketAddr), NetworkError>>,
sender_tx: &Sender<(ProtoEvent, SocketAddr)>,
) -> Result<(), InputEmulationError> {
let backend = server.config.emulation_backend.map(|b| b.into());
log::info!("creating input emulation...");
let mut emulation = tokio::select! {
r = InputEmulation::new(backend) => r?,
_ = server.cancelled() => return Ok(()),
};
server.set_emulation_status(Status::Enabled);
// add clients
for handle in server.active_clients() {
emulation.create(handle).await;
}
let res = do_emulation_session(server, &mut emulation, rx, udp_rx, sender_tx).await;
emulation.terminate().await; // manual drop
res
}
async fn do_emulation_session(
server: &Server,
emulation: &mut InputEmulation,
rx: &mut Receiver<EmulationRequest>,
udp_rx: &mut Receiver<Result<(ProtoEvent, SocketAddr), NetworkError>>,
sender_tx: &Sender<(ProtoEvent, SocketAddr)>,
) -> Result<(), InputEmulationError> {
let mut last_ignored = None;
loop {
tokio::select! {
udp_event = udp_rx.recv() => {
let udp_event = match udp_event.expect("channel closed") {
Ok(e) => e,
Err(e) => {
log::warn!("network error: {e}");
continue;
}
};
handle_incoming_event(server, emulation, sender_tx, &mut last_ignored, udp_event).await?;
}
emulate_event = rx.recv() => {
match emulate_event.expect("channel closed") {
EmulationRequest::Create(h) => { let _ = emulation.create(h).await; },
EmulationRequest::Destroy(h) => emulation.destroy(h).await,
EmulationRequest::ReleaseKeys(c) => emulation.release_keys(c).await?,
}
}
_ = server.notifies.cancel.cancelled() => break Ok(()),
}
}
}
async fn handle_incoming_event(
server: &Server,
emulate: &mut InputEmulation,
sender_tx: &Sender<(ProtoEvent, SocketAddr)>,
last_ignored: &mut Option<SocketAddr>,
event: (ProtoEvent, SocketAddr),
) -> Result<(), EmulationError> {
let (event, addr) = event;
log::trace!("{:20} <-<-<-<------ {addr}", event.to_string());
// get client handle for addr
let Some(handle) =
activate_client_if_exists(&mut server.client_manager.borrow_mut(), addr, last_ignored)
else {
return Ok(());
};
match (event, addr) {
(ProtoEvent::Pong, _) => { /* ignore pong events */ }
(ProtoEvent::Ping, addr) => {
let _ = sender_tx.send((ProtoEvent::Pong, addr));
}
(ProtoEvent::Leave(_), _) => emulate.release_keys(handle).await?,
(ProtoEvent::Ack(_), _) => server.set_state(State::Sending),
(ProtoEvent::Enter(_), _) => {
server.set_state(State::Receiving);
sender_tx
.send((ProtoEvent::Ack(0), addr))
.expect("no channel")
}
(ProtoEvent::Input(e), _) => {
if let State::Receiving = server.get_state() {
log::trace!("{event} => emulate");
emulate.consume(e, handle).await?;
let has_pressed_keys = emulate.has_pressed_keys(handle);
server.update_pressed_keys(handle, has_pressed_keys);
if has_pressed_keys {
server.restart_ping_timer();
}
}
}
}
Ok(())
}
fn activate_client_if_exists(
client_manager: &mut ClientManager,
addr: SocketAddr,
last_ignored: &mut Option<SocketAddr>,
) -> Option<ClientHandle> {
let Some(handle) = client_manager.get_client(addr) else {
// log ignored if it is the first event from the client in a series
if last_ignored.is_none() || last_ignored.is_some() && last_ignored.unwrap() != addr {
log::warn!("ignoring events from client {addr}");
last_ignored.replace(addr);
}
return None;
};
// next event can be logged as ignored again
last_ignored.take();
let (_, client_state) = client_manager.get_mut(handle)?;
// reset ttl for client
client_state.alive = true;
// set addr as new default for this client
client_state.active_addr = Some(addr);
Some(handle)
}

Some files were not shown because too many files have changed in this diff Show More