Compare commits
100 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2ac00c3b48 | |||
| ed7e8bae54 | |||
|
|
a77f3aa8ec | ||
|
|
9764d1975d | ||
|
|
5e68074781 | ||
|
|
f80b1d6ef7 | ||
|
|
346d9cf375 | ||
|
|
6ef2be8776 | ||
|
|
85b6d6ea9d | ||
|
|
1aa8f92cc8 | ||
|
|
996edb0501 | ||
|
|
a30bed4624 | ||
|
|
a2dfa7ac47 | ||
|
|
eebde58826 | ||
|
|
800171efdd | ||
|
|
2b3d5c5bf3 | ||
|
|
1add8e4cb8 | ||
|
|
06674b224f | ||
|
|
d48d9aed3d | ||
|
|
5771f44b0e | ||
|
|
16dc47b570 | ||
|
|
abd9fd4773 | ||
|
|
7c1652e9dd | ||
|
|
0fe41d0b15 | ||
|
|
21461d9a76 | ||
|
|
c6c54c77a7 | ||
|
|
7a806f7b00 | ||
|
|
4fb452c542 | ||
|
|
c71e5c4b8b | ||
|
|
30cfff86b0 | ||
|
|
288345b5d3 | ||
|
|
b4ecfad316 | ||
|
|
dd510bc4a0 | ||
|
|
4916fc59db | ||
|
|
064c3cd242 | ||
|
|
6996fbf958 | ||
|
|
016351e9d2 | ||
|
|
6325782058 | ||
|
|
0184dcb22f | ||
|
|
652ee5aa6c | ||
|
|
88fd6e7314 | ||
|
|
6a812607fe | ||
|
|
f11cefc74c | ||
|
|
7cff8286c8 | ||
|
|
280a654f59 | ||
|
|
d502ca651c | ||
|
|
1985842ff0 | ||
|
|
fc363fe404 | ||
|
|
cc143b59e6 | ||
|
|
02d49bf680 | ||
|
|
601d8c8358 | ||
|
|
0ca1431594 | ||
|
|
48ff3d67a0 | ||
|
|
e94fb986c0 | ||
|
|
7f7604a462 | ||
|
|
e1dbdc9b68 | ||
|
|
6c746b23f1 | ||
|
|
c8e6664a8d | ||
|
|
1803577892 | ||
|
|
4358c0b0dc | ||
|
|
737c9671cf | ||
|
|
925cb745eb | ||
|
|
722678ca4f | ||
|
|
c6fe56479a | ||
|
|
1794150285 | ||
|
|
6a1a7420dc | ||
|
|
13f5df3bf8 | ||
|
|
bb25f2dd37 | ||
|
|
af116f4cfb | ||
|
|
3c2bec8916 | ||
|
|
45801d7cd5 | ||
|
|
4285f6aa1b | ||
|
|
20cea84056 | ||
|
|
b239ca7160 | ||
|
|
e5369af219 | ||
|
|
51108765e3 | ||
|
|
37dff3cb6b | ||
|
|
891fde7051 | ||
|
|
91a37de0fb | ||
|
|
d81b57b1f9 | ||
|
|
0bd7260a0c | ||
|
|
e69bd15733 | ||
|
|
bbb6cc4dc1 | ||
|
|
d254ae5b0d | ||
|
|
809d3a0064 | ||
|
|
feaf02217c | ||
|
|
f5d19a0354 | ||
|
|
342334ac18 | ||
|
|
124152f7ec | ||
|
|
6f03281fc7 | ||
|
|
79a88dc676 | ||
|
|
9c5dcbee10 | ||
|
|
f30b4cf48e | ||
|
|
681d9c4f1d | ||
|
|
2fc5d0825b | ||
|
|
c1491f4a84 | ||
|
|
b96944d4d6 | ||
|
|
61f910adf0 | ||
|
|
518c6ef952 | ||
|
|
1b3d26c465 |
37 changed files with 2561 additions and 1203 deletions
43
.github/workflows/test.yaml
vendored
Normal file
43
.github/workflows/test.yaml
vendored
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
name: Test
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [linux]
|
||||||
|
tags: [[], ['null'], [portal], [uinput], [x11],
|
||||||
|
['null', portal, uinput, x11]]
|
||||||
|
include:
|
||||||
|
- os: windows
|
||||||
|
tags: []
|
||||||
|
- os: windows
|
||||||
|
tags: ['null']
|
||||||
|
env:
|
||||||
|
GOOS: ${{ matrix.os }}
|
||||||
|
GOFLAGS: -tags=${{ join(matrix.tags, ',') }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version: stable
|
||||||
|
check-latest: true
|
||||||
|
- run: sudo apt install libxrandr-dev libxtst-dev libxt-dev
|
||||||
|
if: ${{ contains(matrix.tags, 'x11') }}
|
||||||
|
|
||||||
|
- name: Go Format
|
||||||
|
run: gofmt -s -w . && git diff --exit-code
|
||||||
|
|
||||||
|
- name: Go Mod Tidy
|
||||||
|
run: go mod tidy && git diff --exit-code
|
||||||
|
|
||||||
|
- name: Go Vet
|
||||||
|
run: go vet ./...
|
||||||
|
|
||||||
|
- name: Go Build
|
||||||
|
run: go build ./...
|
||||||
|
|
||||||
|
- name: Go Test (with data race detection)
|
||||||
|
run: go test -race ./...
|
||||||
|
if: ${{ matrix.os == 'linux' }}
|
||||||
11
.gitignore
vendored
Normal file
11
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
# Binaries
|
||||||
|
remote-touchpad
|
||||||
|
|
||||||
|
# Systemd service state
|
||||||
|
.remote-touchpad.service.state
|
||||||
|
|
||||||
|
# Logs and run files
|
||||||
|
.rts-log
|
||||||
|
|
||||||
|
# Environment files
|
||||||
|
.env
|
||||||
78
CHANGELOG.md
78
CHANGELOG.md
|
|
@ -1,5 +1,83 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 1.5.4 (2026-08-02)
|
||||||
|
|
||||||
|
* Improve X11 keyboard input
|
||||||
|
* Update dependencies
|
||||||
|
|
||||||
|
## 1.5.3 (2025-12-24)
|
||||||
|
|
||||||
|
* Update dependencies
|
||||||
|
|
||||||
|
## 1.5.2 (2025-10-12)
|
||||||
|
|
||||||
|
* Update dependencies
|
||||||
|
|
||||||
|
## 1.5.0 (2025-03-18)
|
||||||
|
|
||||||
|
* Add restore token support for the remote desktop portal
|
||||||
|
* Update dependencies
|
||||||
|
|
||||||
|
## 1.4.8 (2024-09-01)
|
||||||
|
|
||||||
|
* Update dependencies
|
||||||
|
|
||||||
|
## 1.4.7 (2024-08-30)
|
||||||
|
|
||||||
|
* Update dependencies
|
||||||
|
|
||||||
|
## 1.4.6 (2024-02-06)
|
||||||
|
|
||||||
|
* Prevent incorrect MIME types
|
||||||
|
|
||||||
|
## 1.4.5 (2024-01-31)
|
||||||
|
|
||||||
|
* Keep text input open after sending
|
||||||
|
* Workaround for virtual keyboard covering buttons (Chromium & iOS)
|
||||||
|
* Enable hardware keyboard input in mouse mode
|
||||||
|
|
||||||
|
## 1.4.4 (2024-01-23)
|
||||||
|
|
||||||
|
* Fix Xwayland detection on newer servers
|
||||||
|
* Fix support for the KDE remote desktop portal
|
||||||
|
* Set correct minimum version for Go
|
||||||
|
* Update dependencies
|
||||||
|
|
||||||
|
## 1.4.3 (2023-10-03)
|
||||||
|
|
||||||
|
* Update dependencies
|
||||||
|
|
||||||
|
## 1.4.2 (2023-05-20)
|
||||||
|
|
||||||
|
* Fix Windows support
|
||||||
|
|
||||||
|
## 1.4.1 (2023-05-11)
|
||||||
|
|
||||||
|
* uinput: Fix vertical scroll direction
|
||||||
|
* Update dependencies
|
||||||
|
|
||||||
|
## 1.4.0 (2023-04-25)
|
||||||
|
|
||||||
|
* Add feedback to buttons
|
||||||
|
* Add numeric keypad
|
||||||
|
* Prevent touch triggering mouse mode
|
||||||
|
|
||||||
|
## 1.3.0 (2023-03-03)
|
||||||
|
|
||||||
|
* Initial support for uinput
|
||||||
|
* Update dependencies
|
||||||
|
|
||||||
|
## 1.2.3 (2022-12-29)
|
||||||
|
|
||||||
|
* Improve XWayland detection
|
||||||
|
* Update dependencies
|
||||||
|
* Flatpak: Add Wayland socket to disable X11 socket
|
||||||
|
|
||||||
|
## 1.2.2 (2022-09-23)
|
||||||
|
|
||||||
|
* Update dependencies
|
||||||
|
* Fix race in RemoteDesktop portal controller
|
||||||
|
|
||||||
## 1.2.1 (2022-06-20)
|
## 1.2.1 (2022-06-20)
|
||||||
|
|
||||||
* Update dependencies
|
* Update dependencies
|
||||||
|
|
|
||||||
41
README.md
41
README.md
|
|
@ -12,8 +12,45 @@ Supports Flatpak's RemoteDesktop portal (for Wayland), Windows and X11.
|
||||||
* [Snap](https://snapcraft.io/remote-touchpad)
|
* [Snap](https://snapcraft.io/remote-touchpad)
|
||||||
* [Windows](https://github.com/Unrud/remote-touchpad/releases/latest)
|
* [Windows](https://github.com/Unrud/remote-touchpad/releases/latest)
|
||||||
* Golang:
|
* Golang:
|
||||||
* Portal & X11: `go install -tags portal,x11 github.com/unrud/remote-touchpad@latest`
|
* Portal & uinput & X11:
|
||||||
* Windows: `go install github.com/unrud/remote-touchpad@latest`
|
|
||||||
|
```sh
|
||||||
|
go install -tags portal,uinput,x11 github.com/unrud/remote-touchpad@latest
|
||||||
|
```
|
||||||
|
* Windows:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go install github.com/unrud/remote-touchpad@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
## Linux User Service Installation
|
||||||
|
|
||||||
|
To install remote-touchpad as a user-specific systemd service (no root access required):
|
||||||
|
|
||||||
|
1. Clone the repository and navigate to the project directory:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git clone https://github.com/Unrud/remote-touchpad.git
|
||||||
|
cd remote-touchpad
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Install the service using the provided wrapper script (no sudo required):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./rts install
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Access the service:
|
||||||
|
|
||||||
|
* View service status: `./rts status`
|
||||||
|
* Get URL: `./rts url`
|
||||||
|
* View logs: `./rts log`
|
||||||
|
* Stop service: `./rts stop`
|
||||||
|
* Start service: `./rts start`
|
||||||
|
* Restart service: `./rts restart`
|
||||||
|
* Uninstall: `./rts uninstall`
|
||||||
|
|
||||||
|
The service will run in the background and maintain a persistent connection with URL and QR code accessible via the `url` command.
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,139 @@
|
||||||
<launchable type="desktop-id">com.github.unrud.RemoteTouchpad.desktop</launchable>
|
<launchable type="desktop-id">com.github.unrud.RemoteTouchpad.desktop</launchable>
|
||||||
<url type="bugtracker">https://github.com/Unrud/remote-touchpad/issues</url>
|
<url type="bugtracker">https://github.com/Unrud/remote-touchpad/issues</url>
|
||||||
<url type="homepage">https://github.com/Unrud/remote-touchpad</url>
|
<url type="homepage">https://github.com/Unrud/remote-touchpad</url>
|
||||||
<developer_name>Unrud</developer_name>
|
<url type="vcs-browser">https://github.com/Unrud/remote-touchpad</url>
|
||||||
|
<developer_name translate="no">Unrud</developer_name>
|
||||||
|
<developer id="io.github.unrud">
|
||||||
|
<name translate="no">Unrud</name>
|
||||||
|
</developer>
|
||||||
<releases>
|
<releases>
|
||||||
|
<release version="1.5.4" date="2026-08-02">
|
||||||
|
<description>
|
||||||
|
<ul>
|
||||||
|
<li>Improve X11 keyboard input</li>
|
||||||
|
<li>Update dependencies</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="1.5.3" date="2025-12-24">
|
||||||
|
<description>
|
||||||
|
<ul>
|
||||||
|
<li>Update dependencies</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="1.5.2" date="2025-10-12">
|
||||||
|
<description>
|
||||||
|
<ul>
|
||||||
|
<li>Update dependencies</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="1.5.0" date="2025-03-18">
|
||||||
|
<description>
|
||||||
|
<ul>
|
||||||
|
<li>Add restore token support for the remote desktop portal</li>
|
||||||
|
<li>Update dependencies</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="1.4.8" date="2024-09-01">
|
||||||
|
<description>
|
||||||
|
<ul>
|
||||||
|
<li>Update dependencies</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="1.4.7" date="2024-08-30">
|
||||||
|
<description>
|
||||||
|
<ul>
|
||||||
|
<li>Update dependencies</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="1.4.6" date="2024-02-06">
|
||||||
|
<description>
|
||||||
|
<ul>
|
||||||
|
<li>Prevent incorrect MIME types</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="1.4.5" date="2024-01-31">
|
||||||
|
<description>
|
||||||
|
<ul>
|
||||||
|
<li>Keep text input open after sending</li>
|
||||||
|
<li>Workaround for virtual keyboard covering buttons (Chromium & iOS)</li>
|
||||||
|
<li>Enable hardware keyboard input in mouse mode</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="1.4.4" date="2024-01-23">
|
||||||
|
<description>
|
||||||
|
<ul>
|
||||||
|
<li>Fix Xwayland detection on newer servers</li>
|
||||||
|
<li>Fix support for the KDE remote desktop portal</li>
|
||||||
|
<li>Set correct minimum version for Go</li>
|
||||||
|
<li>Update dependencies</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="1.4.3" date="2023-10-03">
|
||||||
|
<description>
|
||||||
|
<ul>
|
||||||
|
<li>Update dependencies</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="1.4.2" date="2023-05-20">
|
||||||
|
<description>
|
||||||
|
<ul>
|
||||||
|
<li>Fix Windows support</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="1.4.1" date="2023-05-11">
|
||||||
|
<description>
|
||||||
|
<ul>
|
||||||
|
<li>uinput: Fix vertical scroll direction</li>
|
||||||
|
<li>Update dependencies</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="1.4.0" date="2023-04-25">
|
||||||
|
<description>
|
||||||
|
<ul>
|
||||||
|
<li>Add feedback to buttons</li>
|
||||||
|
<li>Add numeric keypad</li>
|
||||||
|
<li>Prevent touch triggering mouse mode</li>
|
||||||
|
<li>Update dependencies</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="1.3.0" date="2023-03-03">
|
||||||
|
<description>
|
||||||
|
<ul>
|
||||||
|
<li>Initial support for uinput</li>
|
||||||
|
<li>Update dependencies</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="1.2.3" date="2022-12-29">
|
||||||
|
<description>
|
||||||
|
<ul>
|
||||||
|
<li>Improve XWayland detection</li>
|
||||||
|
<li>Update dependencies</li>
|
||||||
|
<li>Flatpak: Add Wayland socket to disable X11 socket</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="1.2.2" date="2022-09-23">
|
||||||
|
<description>
|
||||||
|
<ul>
|
||||||
|
<li>Update dependencies</li>
|
||||||
|
<li>Fix race in RemoteDesktop portal controller</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
<release version="1.2.1" date="2022-06-20">
|
<release version="1.2.1" date="2022-06-20">
|
||||||
<description>
|
<description>
|
||||||
<ul>
|
<ul>
|
||||||
|
|
@ -242,16 +373,16 @@
|
||||||
</releases>
|
</releases>
|
||||||
<screenshots>
|
<screenshots>
|
||||||
<screenshot type="default">
|
<screenshot type="default">
|
||||||
<image>https://raw.githubusercontent.com/Unrud/remote-touchpad/v1.2.1/screenshots/1.png</image>
|
<image>https://raw.githubusercontent.com/Unrud/remote-touchpad/v1.5.4/screenshots/1.png</image>
|
||||||
</screenshot>
|
</screenshot>
|
||||||
<screenshot>
|
<screenshot>
|
||||||
<image>https://raw.githubusercontent.com/Unrud/remote-touchpad/v1.2.1/screenshots/2.png</image>
|
<image>https://raw.githubusercontent.com/Unrud/remote-touchpad/v1.5.4/screenshots/2.png</image>
|
||||||
</screenshot>
|
</screenshot>
|
||||||
<screenshot>
|
<screenshot>
|
||||||
<image>https://raw.githubusercontent.com/Unrud/remote-touchpad/v1.2.1/screenshots/3.png</image>
|
<image>https://raw.githubusercontent.com/Unrud/remote-touchpad/v1.5.4/screenshots/3.png</image>
|
||||||
</screenshot>
|
</screenshot>
|
||||||
<screenshot>
|
<screenshot>
|
||||||
<image>https://raw.githubusercontent.com/Unrud/remote-touchpad/v1.2.1/screenshots/4.png</image>
|
<image>https://raw.githubusercontent.com/Unrud/remote-touchpad/v1.5.4/screenshots/4.png</image>
|
||||||
</screenshot>
|
</screenshot>
|
||||||
</screenshots>
|
</screenshots>
|
||||||
<content_rating type="oars-1.1"/>
|
<content_rating type="oars-1.1"/>
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,24 @@
|
||||||
app-id: com.github.unrud.RemoteTouchpad
|
app-id: com.github.unrud.RemoteTouchpad
|
||||||
runtime: org.freedesktop.Platform
|
runtime: org.freedesktop.Platform
|
||||||
runtime-version: '21.08'
|
runtime-version: '25.08'
|
||||||
sdk: org.freedesktop.Sdk
|
sdk: org.freedesktop.Sdk
|
||||||
command: remote-touchpad
|
command: remote-touchpad
|
||||||
sdk-extensions:
|
sdk-extensions:
|
||||||
- org.freedesktop.Sdk.Extension.golang
|
- org.freedesktop.Sdk.Extension.golang
|
||||||
finish-args:
|
finish-args:
|
||||||
- --socket=x11
|
|
||||||
- --socket=fallback-x11
|
- --socket=fallback-x11
|
||||||
|
- --socket=wayland
|
||||||
|
- --share=ipc # Required by Flathub linter
|
||||||
- --share=network
|
- --share=network
|
||||||
modules:
|
modules:
|
||||||
- name: remote-touchpad
|
- name: remote-touchpad
|
||||||
sources:
|
sources:
|
||||||
- type: git
|
- type: git
|
||||||
path: ..
|
path: ..
|
||||||
- type: shell
|
|
||||||
commands:
|
|
||||||
- . /usr/lib/sdk/golang/enable.sh; go mod vendor -e
|
|
||||||
- type: git
|
- type: git
|
||||||
url: https://github.com/godbus/dbus
|
url: https://github.com/godbus/dbus
|
||||||
tag: v5.1.0
|
tag: v5.2.2
|
||||||
commit: e523abc905595cf17fb0001a7d77eaaddfaa216d
|
commit: a8ac15ba63645f02ffd57f4b443203279ab40b30
|
||||||
dest: vendor/github.com/godbus/dbus/v5
|
dest: vendor/github.com/godbus/dbus/v5
|
||||||
- type: git
|
- type: git
|
||||||
url: https://github.com/skip2/go-qrcode
|
url: https://github.com/skip2/go-qrcode
|
||||||
|
|
@ -28,8 +26,17 @@ modules:
|
||||||
dest: vendor/github.com/skip2/go-qrcode
|
dest: vendor/github.com/skip2/go-qrcode
|
||||||
- type: git
|
- type: git
|
||||||
url: https://github.com/golang/net
|
url: https://github.com/golang/net
|
||||||
commit: 355a448f1bc9c459b279e1cf88367d58b98e186e
|
tag: v0.57.0
|
||||||
|
commit: b8f09f6f062ceb4531b7af4bd17a5c8fe9c4b2b5
|
||||||
dest: vendor/golang.org/x/net
|
dest: vendor/golang.org/x/net
|
||||||
|
- type: git
|
||||||
|
url: https://github.com/golang/sys
|
||||||
|
tag: v0.47.0
|
||||||
|
commit: 9e7e939dcafac07e8ab4cffa6e5fc74908413f00
|
||||||
|
dest: vendor/golang.org/x/sys
|
||||||
|
- type: file
|
||||||
|
path: modules.txt
|
||||||
|
dest: vendor
|
||||||
buildsystem: simple
|
buildsystem: simple
|
||||||
build-commands:
|
build-commands:
|
||||||
- . /usr/lib/sdk/golang/enable.sh; go build -tags portal,x11
|
- . /usr/lib/sdk/golang/enable.sh; go build -tags portal,x11
|
||||||
|
|
|
||||||
17
flatpak/modules.txt
Normal file
17
flatpak/modules.txt
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
# github.com/bendahl/uinput v1.7.0
|
||||||
|
## explicit; go 1.13
|
||||||
|
github.com/bendahl/uinput
|
||||||
|
# github.com/godbus/dbus/v5 v5.2.2
|
||||||
|
## explicit; go 1.20
|
||||||
|
github.com/godbus/dbus/v5
|
||||||
|
# github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||||
|
## explicit; go 1.13
|
||||||
|
github.com/skip2/go-qrcode
|
||||||
|
github.com/skip2/go-qrcode/bitset
|
||||||
|
github.com/skip2/go-qrcode/reedsolomon
|
||||||
|
# golang.org/x/net v0.57.0
|
||||||
|
## explicit; go 1.25.0
|
||||||
|
golang.org/x/net/websocket
|
||||||
|
# golang.org/x/sys v0.47.0
|
||||||
|
## explicit; go 1.25.0
|
||||||
|
golang.org/x/sys/unix
|
||||||
9
go.mod
9
go.mod
|
|
@ -1,9 +1,12 @@
|
||||||
module github.com/unrud/remote-touchpad
|
module github.com/unrud/remote-touchpad
|
||||||
|
|
||||||
go 1.17
|
go 1.25.5
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/godbus/dbus/v5 v5.1.0
|
github.com/bendahl/uinput v1.7.0
|
||||||
|
github.com/godbus/dbus/v5 v5.2.2
|
||||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||||
golang.org/x/net v0.0.0-20220617184016-355a448f1bc9
|
golang.org/x/net v0.57.0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
require golang.org/x/sys v0.47.0 // indirect
|
||||||
|
|
|
||||||
17
go.sum
17
go.sum
|
|
@ -1,11 +1,10 @@
|
||||||
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
github.com/bendahl/uinput v1.7.0 h1:nA4fm8Wu8UYNOPykIZm66nkWEyvxzfmJ8YC02PM40jg=
|
||||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
github.com/bendahl/uinput v1.7.0/go.mod h1:Np7w3DINc9wB83p12fTAM3DPPhFnAKP0WTXRqCQJ6Z8=
|
||||||
|
github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
|
||||||
|
github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
|
||||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
||||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
|
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
|
||||||
golang.org/x/net v0.0.0-20220617184016-355a448f1bc9 h1:Yqz/iviulwKwAREEeUd3nbBFn0XuyJqkoft2IlrvOhc=
|
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
|
||||||
golang.org/x/net v0.0.0-20220617184016-355a448f1bc9/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
|
||||||
|
|
|
||||||
3
host.go
3
host.go
|
|
@ -74,7 +74,8 @@ func findDefaultHost() string {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for _, linkLocalPrefix := range []string{
|
for _, linkLocalPrefix := range []string{
|
||||||
"169.254.", "fe8", "fe9", "fea", "feb"} {
|
"169.254.", "fe8", "fe9", "fea", "feb",
|
||||||
|
} {
|
||||||
if strings.HasPrefix(ip.String(), linkLocalPrefix) {
|
if strings.HasPrefix(ip.String(), linkLocalPrefix) {
|
||||||
addIP(20, ip)
|
addIP(20, ip)
|
||||||
continue addrs
|
continue addrs
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,10 @@ package inputcontrol
|
||||||
|
|
||||||
import "sort"
|
import "sort"
|
||||||
|
|
||||||
type PointerButton int
|
type (
|
||||||
type Key int
|
PointerButton int
|
||||||
|
Key int
|
||||||
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
PointerButtonLeft PointerButton = iota
|
PointerButtonLeft PointerButton = iota
|
||||||
|
|
@ -70,11 +72,15 @@ func RegisterController(name string, init func() (Controller, error), priority i
|
||||||
}
|
}
|
||||||
|
|
||||||
type UnsupportedPlatformError struct {
|
type UnsupportedPlatformError struct {
|
||||||
err error
|
Err error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e UnsupportedPlatformError) Error() string {
|
func (e *UnsupportedPlatformError) Error() string {
|
||||||
return e.err.Error()
|
return e.Err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *UnsupportedPlatformError) Unwrap() error {
|
||||||
|
return e.Err
|
||||||
}
|
}
|
||||||
|
|
||||||
type Controller interface {
|
type Controller interface {
|
||||||
|
|
|
||||||
66
inputcontrol/controller_null.go
Normal file
66
inputcontrol/controller_null.go
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
//go:build null
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 Unrud <unrud@outlook.com>
|
||||||
|
*
|
||||||
|
* This file is part of Remote-Touchpad.
|
||||||
|
*
|
||||||
|
* Remote-Touchpad is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Remote-Touchpad is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Remote-Touchpad. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package inputcontrol
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type nullController struct{}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
RegisterController("null", InitNullController, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
func InitNullController() (Controller, error) {
|
||||||
|
return &nullController{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *nullController) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *nullController) KeyboardText(text string) error {
|
||||||
|
log.Printf("KeyboardText(text: %#v)", text)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *nullController) KeyboardKey(key Key) error {
|
||||||
|
log.Printf("KeyboardKey(key: %#v)", key)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *nullController) PointerButton(button PointerButton, press bool) error {
|
||||||
|
log.Printf("PointerButton(button: %#v, press: %#v)", button, press)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *nullController) PointerMove(deltaX, deltaY int) error {
|
||||||
|
log.Printf("PointerMove(deltaX: %#v, deltaY: %#v)", deltaX, deltaY)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *nullController) PointerScroll(deltaHorizontal, deltaVertical int, finish bool) error {
|
||||||
|
log.Printf("PointerScroll(deltaHorizontal: %#v, deltaVertical: %#v, finish: %#v)",
|
||||||
|
deltaHorizontal, deltaVertical, finish)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
@ -22,10 +22,20 @@
|
||||||
package inputcontrol
|
package inputcontrol
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"crypto/hkdf"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/sha256"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"slices"
|
||||||
|
|
||||||
"github.com/godbus/dbus/v5"
|
"github.com/godbus/dbus/v5"
|
||||||
"sync"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
@ -35,6 +45,8 @@ const (
|
||||||
btnReleased uint32 = 0
|
btnReleased uint32 = 0
|
||||||
btnPressed uint32 = 1
|
btnPressed uint32 = 1
|
||||||
|
|
||||||
|
untilRevoked uint32 = 2
|
||||||
|
|
||||||
// linux/input-event-codes.h
|
// linux/input-event-codes.h
|
||||||
btnLeft int32 = 0x110
|
btnLeft int32 = 0x110
|
||||||
btnRight int32 = 0x111
|
btnRight int32 = 0x111
|
||||||
|
|
@ -43,9 +55,8 @@ const (
|
||||||
|
|
||||||
type portalController struct {
|
type portalController struct {
|
||||||
bus *dbus.Conn
|
bus *dbus.Conn
|
||||||
remoteDesktop dbus.BusObject
|
portalDesktop dbus.BusObject
|
||||||
sessionHandle dbus.ObjectPath
|
sessionHandle dbus.ObjectPath
|
||||||
lock sync.RWMutex
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|
@ -55,7 +66,7 @@ func init() {
|
||||||
func InitPortalController() (Controller, error) {
|
func InitPortalController() (Controller, error) {
|
||||||
bus, err := dbus.SessionBusPrivate()
|
bus, err := dbus.SessionBusPrivate()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, UnsupportedPlatformError{err}
|
return nil, &UnsupportedPlatformError{err}
|
||||||
}
|
}
|
||||||
cleanupBus := true
|
cleanupBus := true
|
||||||
defer func() {
|
defer func() {
|
||||||
|
|
@ -65,92 +76,216 @@ func InitPortalController() (Controller, error) {
|
||||||
}()
|
}()
|
||||||
err = bus.Auth(nil)
|
err = bus.Auth(nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, UnsupportedPlatformError{err}
|
return nil, &UnsupportedPlatformError{err}
|
||||||
}
|
}
|
||||||
err = bus.Hello()
|
err = bus.Hello()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, UnsupportedPlatformError{err}
|
return nil, &UnsupportedPlatformError{err}
|
||||||
}
|
}
|
||||||
remoteDesktop := bus.Object("org.freedesktop.portal.Desktop",
|
portalDesktop := bus.Object("org.freedesktop.portal.Desktop",
|
||||||
"/org/freedesktop/portal/desktop")
|
"/org/freedesktop/portal/desktop")
|
||||||
availableDeviceTypesV, err := remoteDesktop.GetProperty(
|
remoteDesktopVersionV, err := portalDesktop.GetProperty(
|
||||||
|
"org.freedesktop.portal.RemoteDesktop.version")
|
||||||
|
if err != nil {
|
||||||
|
return nil, &UnsupportedPlatformError{
|
||||||
|
fmt.Errorf("getting 'version' failed: %w", err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
remoteDesktopVersion, ok := remoteDesktopVersionV.Value().(uint32)
|
||||||
|
if !ok {
|
||||||
|
return nil, &UnsupportedPlatformError{
|
||||||
|
errors.New("unexpected 'version' type"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
restoreTokenStore, err := func() (*secretStore, error) {
|
||||||
|
if remoteDesktopVersion < 2 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
cacheDirectory, err := os.UserCacheDir()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(cacheDirectory, 0o700); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
secret, err := retrieveSecret(bus)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return newSecretStore(secret,
|
||||||
|
filepath.Join(cacheDirectory, "remote-touchpad.portal-restore-token.bin"))
|
||||||
|
}()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Skipping restore token: %v", err)
|
||||||
|
}
|
||||||
|
availableDeviceTypesV, err := portalDesktop.GetProperty(
|
||||||
"org.freedesktop.portal.RemoteDesktop.AvailableDeviceTypes")
|
"org.freedesktop.portal.RemoteDesktop.AvailableDeviceTypes")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, UnsupportedPlatformError{err}
|
return nil, &UnsupportedPlatformError{
|
||||||
|
fmt.Errorf("getting 'AvailableDeviceTypes' failed: %w", err),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
availableDeviceTypes, ok := availableDeviceTypesV.Value().(uint32)
|
availableDeviceTypes, ok := availableDeviceTypesV.Value().(uint32)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, UnsupportedPlatformError{errors.New(
|
return nil, &UnsupportedPlatformError{
|
||||||
"unexpected 'AvailableDeviceTypes' return type")}
|
errors.New("unexpected 'AvailableDeviceTypes' return type"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if availableDeviceTypes&deviceKeyboard == 0 ||
|
if availableDeviceTypes&deviceKeyboard == 0 ||
|
||||||
availableDeviceTypes&devicePointer == 0 {
|
availableDeviceTypes&devicePointer == 0 {
|
||||||
return nil, UnsupportedPlatformError{errors.New(
|
return nil, &UnsupportedPlatformError{
|
||||||
"keyboard or pointer source type not supported")}
|
errors.New("keyboard or pointer source type not supported"),
|
||||||
}
|
}
|
||||||
inVardict := make(map[string]dbus.Variant)
|
}
|
||||||
inVardict["session_handle_token"] = dbus.MakeVariant("t")
|
createSessionResults, err := checkResponse(getResponse(bus, portalDesktop,
|
||||||
result, outVardict, err := getResponse(bus, remoteDesktop,
|
"org.freedesktop.portal.RemoteDesktop.CreateSession", 0,
|
||||||
"org.freedesktop.portal.RemoteDesktop.CreateSession", 0, inVardict)
|
map[string]dbus.Variant{"session_handle_token": dbus.MakeVariant("t")},
|
||||||
|
))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, UnsupportedPlatformError{err}
|
return nil, &UnsupportedPlatformError{
|
||||||
|
fmt.Errorf("calling 'CreateSession' failed: %w", err),
|
||||||
}
|
}
|
||||||
if result != 0 {
|
|
||||||
return nil, UnsupportedPlatformError{errors.New(fmt.Sprintf(
|
|
||||||
"Calling 'CreateSession' failed (%v)", result))}
|
|
||||||
}
|
}
|
||||||
sessionHandleV, ok := outVardict["session_handle"]
|
sessionHandleString, ok := createSessionResults["session_handle"].Value().(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, UnsupportedPlatformError{errors.New(
|
return nil, &UnsupportedPlatformError{
|
||||||
"'session_handle' missing from 'CreateSession' return value")}
|
errors.New("unexpected 'session_handle' type in 'CreateSession' return value"),
|
||||||
}
|
}
|
||||||
sessionHandleS, ok := sessionHandleV.Value().(string)
|
|
||||||
if !ok {
|
|
||||||
return nil, UnsupportedPlatformError{errors.New(
|
|
||||||
"unexpected 'session_handle' type in 'CreateSession' return value")}
|
|
||||||
}
|
}
|
||||||
sessionHandle := dbus.ObjectPath(sessionHandleS)
|
sessionHandle := dbus.ObjectPath(sessionHandleString)
|
||||||
inVardict = make(map[string]dbus.Variant)
|
selectDevicesOptions := map[string]dbus.Variant{
|
||||||
inVardict["type"] = dbus.MakeVariant(deviceKeyboard | devicePointer)
|
"types": dbus.MakeVariant(deviceKeyboard | devicePointer),
|
||||||
result, outVardict, err = getResponse(bus, remoteDesktop,
|
}
|
||||||
"org.freedesktop.portal.RemoteDesktop.SelectDevices", 0, sessionHandle, inVardict)
|
if restoreTokenStore != nil {
|
||||||
|
if restoreToken, err := restoreTokenStore.Load(); err != nil {
|
||||||
|
if !errors.Is(err, os.ErrNotExist) {
|
||||||
|
log.Printf("Failed to load restore token: %v", err)
|
||||||
|
}
|
||||||
|
} else if len(restoreToken) > 0 {
|
||||||
|
selectDevicesOptions["restore_token"] = dbus.MakeVariant(string(restoreToken))
|
||||||
|
}
|
||||||
|
selectDevicesOptions["persist_mode"] = dbus.MakeVariant(untilRevoked)
|
||||||
|
}
|
||||||
|
_, err = checkResponse(getResponse(bus, portalDesktop,
|
||||||
|
"org.freedesktop.portal.RemoteDesktop.SelectDevices", 0,
|
||||||
|
sessionHandle, selectDevicesOptions,
|
||||||
|
))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, UnsupportedPlatformError{err}
|
return nil, &UnsupportedPlatformError{
|
||||||
|
fmt.Errorf("calling 'SelectDevices' failed: %w", err),
|
||||||
}
|
}
|
||||||
if result != 0 {
|
|
||||||
return nil, UnsupportedPlatformError{errors.New(fmt.Sprintf(
|
|
||||||
"Calling 'SelectDevices' failed (%v)", result))}
|
|
||||||
}
|
}
|
||||||
inVardict = make(map[string]dbus.Variant)
|
startResponseStatus, startResults, err := getResponse(bus, portalDesktop,
|
||||||
result, outVardict, err = getResponse(bus, remoteDesktop,
|
"org.freedesktop.portal.RemoteDesktop.Start", 0,
|
||||||
"org.freedesktop.portal.RemoteDesktop.Start", 0, sessionHandle, "", inVardict)
|
sessionHandle, "", map[string]dbus.Variant{},
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, UnsupportedPlatformError{err}
|
return nil, &UnsupportedPlatformError{
|
||||||
|
fmt.Errorf("calling 'Start' failed: %w", err),
|
||||||
}
|
}
|
||||||
if result != 0 {
|
}
|
||||||
|
if startResponseStatus != 0 {
|
||||||
return nil, errors.New("keyboard or pointer access denied")
|
return nil, errors.New("keyboard or pointer access denied")
|
||||||
}
|
}
|
||||||
devicesV, ok := outVardict["devices"]
|
if restoreToken, _ := startResults["restore_token"].Value().(string); restoreTokenStore != nil {
|
||||||
if !ok {
|
if err := restoreTokenStore.Store([]byte(restoreToken)); err != nil {
|
||||||
return nil, UnsupportedPlatformError{errors.New(
|
log.Printf("Failed to store restore token: %v", err)
|
||||||
"'devices' missing from 'Start' return value")}
|
|
||||||
}
|
}
|
||||||
devices, ok := devicesV.Value().(uint32)
|
}
|
||||||
|
devices, ok := startResults["devices"].Value().(uint32)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, UnsupportedPlatformError{errors.New(
|
return nil, &UnsupportedPlatformError{
|
||||||
"unexpected 'devices' type in 'Start' return value")}
|
errors.New("unexpected 'devices' type in 'Start' return value"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if devices&deviceKeyboard == 0 || devices&devicePointer == 0 {
|
if devices&deviceKeyboard == 0 || devices&devicePointer == 0 {
|
||||||
return nil, errors.New("keyboard or pointer access denied")
|
return nil, errors.New("keyboard or pointer access denied")
|
||||||
}
|
}
|
||||||
cleanupBus = false
|
cleanupBus = false
|
||||||
return &portalController{bus: bus, remoteDesktop: remoteDesktop,
|
return &portalController{
|
||||||
sessionHandle: sessionHandle}, nil
|
bus: bus, portalDesktop: portalDesktop,
|
||||||
|
sessionHandle: sessionHandle,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func retrieveSecret(bus *dbus.Conn) ([]byte, error) {
|
||||||
|
portalDesktop := bus.Object("org.freedesktop.portal.Desktop",
|
||||||
|
"/org/freedesktop/portal/desktop")
|
||||||
|
secretReader, secretWriter, err := os.Pipe()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer secretReader.Close()
|
||||||
|
defer secretWriter.Close()
|
||||||
|
if _, err := checkResponse(getResponse(bus, portalDesktop,
|
||||||
|
"org.freedesktop.portal.Secret.RetrieveSecret", 0,
|
||||||
|
dbus.UnixFD(secretWriter.Fd()), map[string]dbus.Variant{},
|
||||||
|
)); err != nil {
|
||||||
|
return nil, fmt.Errorf("calling 'RetrieveSecret' failed: %w", err)
|
||||||
|
}
|
||||||
|
if err := secretWriter.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
secret, err := io.ReadAll(secretReader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(secret) < 16 {
|
||||||
|
return nil, fmt.Errorf("'RetrieveSecret' returned too few bytes (%d)", len(secret))
|
||||||
|
}
|
||||||
|
return secret, err
|
||||||
|
}
|
||||||
|
|
||||||
|
type secretStore struct {
|
||||||
|
aesgcm cipher.AEAD
|
||||||
|
filename string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSecretStore(key []byte, filename string) (*secretStore, error) {
|
||||||
|
derivedKey, err := hkdf.Key(sha256.New, key, nil, "", 32)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
block, err := aes.NewCipher(derivedKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
aesgcm, err := cipher.NewGCM(block)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &secretStore{
|
||||||
|
aesgcm: aesgcm,
|
||||||
|
filename: filename,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *secretStore) Load() ([]byte, error) {
|
||||||
|
data, err := os.ReadFile(s.filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(data) < s.aesgcm.NonceSize() {
|
||||||
|
return nil, errors.New("invalid ciphertext")
|
||||||
|
}
|
||||||
|
nonce := data[:s.aesgcm.NonceSize()]
|
||||||
|
ciphertext := data[len(nonce):]
|
||||||
|
return s.aesgcm.Open(nil, nonce, ciphertext, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *secretStore) Store(data []byte) error {
|
||||||
|
nonce := make([]byte, s.aesgcm.NonceSize())
|
||||||
|
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ciphertext := s.aesgcm.Seal(nil, nonce, data, nil)
|
||||||
|
return os.WriteFile(s.filename, slices.Concat(nonce, ciphertext), 0o600)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getResponse(bus *dbus.Conn, object dbus.BusObject, method string,
|
func getResponse(bus *dbus.Conn, object dbus.BusObject, method string,
|
||||||
flags dbus.Flags, args ...interface{}) (uint32, map[string]dbus.Variant, error) {
|
flags dbus.Flags, args ...interface{},
|
||||||
ch := make(chan *dbus.Signal)
|
) (uint32, map[string]dbus.Variant, error) {
|
||||||
|
ch := make(chan *dbus.Signal, 512)
|
||||||
bus.Signal(ch)
|
bus.Signal(ch)
|
||||||
defer bus.RemoveSignal(ch)
|
defer bus.RemoveSignal(ch)
|
||||||
var requestPath dbus.ObjectPath
|
var requestPath dbus.ObjectPath
|
||||||
|
|
@ -161,47 +296,39 @@ func getResponse(bus *dbus.Conn, object dbus.BusObject, method string,
|
||||||
s := <-ch
|
s := <-ch
|
||||||
if s.Path == requestPath && s.Name == "org.freedesktop.portal.Request.Response" {
|
if s.Path == requestPath && s.Name == "org.freedesktop.portal.Request.Response" {
|
||||||
if len(s.Body) != 2 {
|
if len(s.Body) != 2 {
|
||||||
return 0, nil, errors.New("unexpected 'Response' return length")
|
return 0, nil, fmt.Errorf("unexpected 'Response' return length (%d)", len(s.Body))
|
||||||
}
|
}
|
||||||
result, ok := s.Body[0].(uint32)
|
responseStatus, ok := s.Body[0].(uint32)
|
||||||
if !ok {
|
if !ok {
|
||||||
return 0, nil, errors.New("unexpected 'Response' return type")
|
return 0, nil, errors.New("unexpected 'Response' return type")
|
||||||
}
|
}
|
||||||
outVardict, ok := s.Body[1].(map[string]dbus.Variant)
|
results, ok := s.Body[1].(map[string]dbus.Variant)
|
||||||
if !ok {
|
if !ok {
|
||||||
return 0, nil, errors.New("unexpected 'Response' return type")
|
return 0, nil, errors.New("unexpected 'Response' return type")
|
||||||
}
|
}
|
||||||
return result, outVardict, nil
|
return responseStatus, results, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func checkResponse(responseStatus uint32, results map[string]dbus.Variant, err error) (map[string]dbus.Variant, error) {
|
||||||
|
if err == nil && responseStatus != 0 {
|
||||||
|
err = fmt.Errorf("unexpected 'Response' status (%d)", responseStatus)
|
||||||
|
}
|
||||||
|
return results, err
|
||||||
|
}
|
||||||
|
|
||||||
func (p *portalController) Close() error {
|
func (p *portalController) Close() error {
|
||||||
p.lock.Lock()
|
return p.bus.Close()
|
||||||
defer p.lock.Unlock()
|
|
||||||
if p.bus == nil {
|
|
||||||
return errors.New("dbus connection closed")
|
|
||||||
}
|
|
||||||
if err := p.bus.Close(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
p.bus = nil
|
|
||||||
p.remoteDesktop = nil
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *portalController) keyboardKeys(keys []Keysym) error {
|
func (p *portalController) keyboardKeys(keys []Keysym) error {
|
||||||
p.lock.RLock()
|
|
||||||
defer p.lock.RUnlock()
|
|
||||||
if p.bus == nil {
|
|
||||||
return errors.New("dbus connection closed")
|
|
||||||
}
|
|
||||||
inVardict := make(map[string]dbus.Variant)
|
|
||||||
for _, keysym := range keys {
|
for _, keysym := range keys {
|
||||||
for _, state := range [...]uint32{btnPressed, btnReleased} {
|
for _, state := range [...]uint32{btnPressed, btnReleased} {
|
||||||
if err := p.remoteDesktop.Call(
|
if err := p.portalDesktop.Call(
|
||||||
"org.freedesktop.portal.RemoteDesktop.NotifyKeyboardKeysym",
|
"org.freedesktop.portal.RemoteDesktop.NotifyKeyboardKeysym", 0,
|
||||||
0, p.sessionHandle, inVardict, keysym, state).Store(); err != nil {
|
p.sessionHandle, map[string]dbus.Variant{}, keysym, state,
|
||||||
|
).Store(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -231,57 +358,42 @@ func (p *portalController) KeyboardKey(key Key) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *portalController) PointerButton(button PointerButton, press bool) error {
|
func (p *portalController) PointerButton(button PointerButton, press bool) error {
|
||||||
p.lock.RLock()
|
|
||||||
defer p.lock.RUnlock()
|
|
||||||
if p.bus == nil {
|
|
||||||
return errors.New("dbus connection closed")
|
|
||||||
}
|
|
||||||
var btn int32
|
var btn int32
|
||||||
if button == PointerButtonLeft {
|
switch button {
|
||||||
|
case PointerButtonLeft:
|
||||||
btn = btnLeft
|
btn = btnLeft
|
||||||
} else if button == PointerButtonMiddle {
|
case PointerButtonMiddle:
|
||||||
btn = btnMiddle
|
btn = btnMiddle
|
||||||
} else if button == PointerButtonRight {
|
case PointerButtonRight:
|
||||||
btn = btnRight
|
btn = btnRight
|
||||||
} else {
|
default:
|
||||||
return errors.New("unsupported pointer button")
|
return fmt.Errorf("unsupported pointer button: %#v", button)
|
||||||
}
|
}
|
||||||
state := btnReleased
|
state := btnReleased
|
||||||
if press {
|
if press {
|
||||||
state = btnPressed
|
state = btnPressed
|
||||||
}
|
}
|
||||||
inVardict := make(map[string]dbus.Variant)
|
if err := p.portalDesktop.Call("org.freedesktop.portal.RemoteDesktop.NotifyPointerButton", 0,
|
||||||
if err := p.remoteDesktop.Call("org.freedesktop.portal.RemoteDesktop.NotifyPointerButton",
|
p.sessionHandle, map[string]dbus.Variant{}, btn, state,
|
||||||
0, p.sessionHandle, inVardict, btn, state).Store(); err != nil {
|
).Store(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *portalController) PointerMove(deltaX, deltaY int) error {
|
func (p *portalController) PointerMove(deltaX, deltaY int) error {
|
||||||
p.lock.RLock()
|
if err := p.portalDesktop.Call("org.freedesktop.portal.RemoteDesktop.NotifyPointerMotion", 0,
|
||||||
defer p.lock.RUnlock()
|
p.sessionHandle, map[string]dbus.Variant{}, float64(deltaX), float64(deltaY),
|
||||||
if p.bus == nil {
|
).Store(); err != nil {
|
||||||
return errors.New("dbus connection closed")
|
|
||||||
}
|
|
||||||
inVardict := make(map[string]dbus.Variant)
|
|
||||||
if err := p.remoteDesktop.Call("org.freedesktop.portal.RemoteDesktop.NotifyPointerMotion",
|
|
||||||
0, p.sessionHandle, inVardict, float64(deltaX), float64(deltaY)).Store(); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *portalController) PointerScroll(deltaHorizontal, deltaVertical int, finish bool) error {
|
func (p *portalController) PointerScroll(deltaHorizontal, deltaVertical int, finish bool) error {
|
||||||
p.lock.RLock()
|
if err := p.portalDesktop.Call("org.freedesktop.portal.RemoteDesktop.NotifyPointerAxis", 0,
|
||||||
defer p.lock.RUnlock()
|
p.sessionHandle, map[string]dbus.Variant{"finish": dbus.MakeVariant(finish)}, float64(deltaHorizontal), float64(deltaVertical),
|
||||||
if p.bus == nil {
|
).Store(); err != nil {
|
||||||
return errors.New("dbus connection closed")
|
|
||||||
}
|
|
||||||
inVardict := make(map[string]dbus.Variant)
|
|
||||||
inVardict["finish"] = dbus.MakeVariant(finish)
|
|
||||||
if err := p.remoteDesktop.Call("org.freedesktop.portal.RemoteDesktop.NotifyPointerAxis",
|
|
||||||
0, p.sessionHandle, inVardict, float64(deltaHorizontal), float64(deltaVertical)).Store(); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|
|
||||||
200
inputcontrol/controller_uinput.go
Normal file
200
inputcontrol/controller_uinput.go
Normal file
|
|
@ -0,0 +1,200 @@
|
||||||
|
//go:build uinput
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 De_Coder github.com/ps100000
|
||||||
|
* Copyright (c) 2023 Unrud <unrud@outlook.com>
|
||||||
|
*
|
||||||
|
* This file is part of Remote-Touchpad.
|
||||||
|
*
|
||||||
|
* Remote-Touchpad is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Remote-Touchpad is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Remote-Touchpad. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package inputcontrol
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/bendahl/uinput"
|
||||||
|
)
|
||||||
|
|
||||||
|
const shiftKeysDelay time.Duration = 50 * time.Millisecond
|
||||||
|
|
||||||
|
type uinputController struct {
|
||||||
|
keymap *Keymap
|
||||||
|
keyboard uinput.Keyboard
|
||||||
|
mouse uinput.Mouse
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
RegisterController("uinput", InitUinputController, 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
func InitUinputController() (Controller, error) {
|
||||||
|
keymapName, keymapSet := os.LookupEnv("REMOTE_TOUCHPAD_UINPUT_KEYMAP")
|
||||||
|
if !keymapSet {
|
||||||
|
keymapName = "defkeymap"
|
||||||
|
}
|
||||||
|
keymap, err := LoadKeymap(keymapName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
keyboard, err := uinput.CreateKeyboard("/dev/uinput", []byte("remote-touchpad-keyboard"))
|
||||||
|
if errors.Is(err, os.ErrNotExist) || errors.Is(err, os.ErrPermission) {
|
||||||
|
err = &UnsupportedPlatformError{err}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
mouse, err := uinput.CreateMouse("/dev/uinput", []byte("remote-touchpad-mouse"))
|
||||||
|
if errors.Is(err, os.ErrNotExist) || errors.Is(err, os.ErrPermission) {
|
||||||
|
err = &UnsupportedPlatformError{err}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
keyboard.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !keymapSet {
|
||||||
|
log.Print("Hint: Set the keyboard mapping with the REMOTE_TOUCHPAD_UINPUT_KEYMAP environment variable")
|
||||||
|
}
|
||||||
|
return &uinputController{keymap, keyboard, mouse}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *uinputController) Close() error {
|
||||||
|
return errors.Join(p.keyboard.Close(), p.mouse.Close())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *uinputController) KeyboardText(text string) error {
|
||||||
|
var activeShiftKeys KeyCombo
|
||||||
|
updateShiftKeys := func(keyCombo KeyCombo) error {
|
||||||
|
if activeShiftKeys.ShiftKeys == keyCombo.ShiftKeys {
|
||||||
|
activeShiftKeys.Key = keyCombo.Key
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if activeShiftKeys.Key != 0 {
|
||||||
|
time.Sleep(shiftKeysDelay)
|
||||||
|
activeShiftKeys.Key = 0
|
||||||
|
}
|
||||||
|
for i := range keyCombo.ShiftKeys {
|
||||||
|
if activeShiftKeys.ShiftKeys[i] != keyCombo.ShiftKeys[i] {
|
||||||
|
if activeShiftKeys.ShiftKeys[i] != 0 {
|
||||||
|
if err := p.keyboard.KeyUp(activeShiftKeys.ShiftKeys[i]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
activeShiftKeys.ShiftKeys[i] = 0
|
||||||
|
}
|
||||||
|
if keyCombo.ShiftKeys[i] != 0 {
|
||||||
|
if err := p.keyboard.KeyDown(keyCombo.ShiftKeys[i]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
activeShiftKeys.ShiftKeys[i] = keyCombo.ShiftKeys[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if keyCombo.Key != 0 {
|
||||||
|
activeShiftKeys.Key = keyCombo.Key
|
||||||
|
time.Sleep(shiftKeysDelay)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
defer updateShiftKeys(KeyCombo{})
|
||||||
|
for _, runeValue := range text {
|
||||||
|
keyCombo, found := p.keymap.Get(runeValue)
|
||||||
|
if !found {
|
||||||
|
return fmt.Errorf("unsupported rune: %q", runeValue)
|
||||||
|
}
|
||||||
|
if err := updateShiftKeys(keyCombo); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := p.keyboard.KeyPress(keyCombo.Key); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return updateShiftKeys(KeyCombo{Key: 1})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *uinputController) KeyboardKey(key Key) error {
|
||||||
|
var uinputKey int
|
||||||
|
switch key {
|
||||||
|
case KeyBackSpace:
|
||||||
|
uinputKey = uinput.KeyBackspace
|
||||||
|
case KeyReturn:
|
||||||
|
uinputKey = uinput.KeyEnter
|
||||||
|
case KeyDelete:
|
||||||
|
uinputKey = uinput.KeyDelete
|
||||||
|
case KeyHome:
|
||||||
|
uinputKey = uinput.KeyHome
|
||||||
|
case KeyLeft:
|
||||||
|
uinputKey = uinput.KeyLeft
|
||||||
|
case KeyUp:
|
||||||
|
uinputKey = uinput.KeyUp
|
||||||
|
case KeyRight:
|
||||||
|
uinputKey = uinput.KeyRight
|
||||||
|
case KeyDown:
|
||||||
|
uinputKey = uinput.KeyDown
|
||||||
|
case KeyEnd:
|
||||||
|
uinputKey = uinput.KeyEnd
|
||||||
|
case KeySuper:
|
||||||
|
uinputKey = uinput.KeyLeftmeta
|
||||||
|
case KeyVolumeMute:
|
||||||
|
uinputKey = uinput.KeyMute
|
||||||
|
case KeyVolumeDown:
|
||||||
|
uinputKey = uinput.KeyVolumedown
|
||||||
|
case KeyVolumeUp:
|
||||||
|
uinputKey = uinput.KeyVolumeup
|
||||||
|
case KeyMediaPlayPause:
|
||||||
|
uinputKey = uinput.KeyPlaypause
|
||||||
|
case KeyMediaPrevTrack:
|
||||||
|
uinputKey = uinput.KeyPrevioussong
|
||||||
|
case KeyMediaNextTrack:
|
||||||
|
uinputKey = uinput.KeyNextsong
|
||||||
|
case KeyBrowserBack:
|
||||||
|
uinputKey = uinput.KeyBack
|
||||||
|
case KeyBrowserForward:
|
||||||
|
uinputKey = uinput.KeyForward
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported key: %#v", key)
|
||||||
|
}
|
||||||
|
return p.keyboard.KeyPress(uinputKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *uinputController) PointerButton(button PointerButton, press bool) error {
|
||||||
|
switch {
|
||||||
|
case button == PointerButtonLeft && press:
|
||||||
|
return p.mouse.LeftPress()
|
||||||
|
case button == PointerButtonLeft && !press:
|
||||||
|
return p.mouse.LeftRelease()
|
||||||
|
case button == PointerButtonRight && press:
|
||||||
|
return p.mouse.RightPress()
|
||||||
|
case button == PointerButtonRight && !press:
|
||||||
|
return p.mouse.RightRelease()
|
||||||
|
case button == PointerButtonMiddle && press:
|
||||||
|
return p.mouse.MiddlePress()
|
||||||
|
case button == PointerButtonMiddle && !press:
|
||||||
|
return p.mouse.MiddleRelease()
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported pointer button: %#v", button)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *uinputController) PointerMove(deltaX, deltaY int) error {
|
||||||
|
return p.mouse.Move(int32(deltaX), int32(deltaY))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *uinputController) PointerScroll(deltaHorizontal, deltaVertical int, finish bool) error {
|
||||||
|
return errors.Join(p.mouse.Wheel(false, int32(-deltaVertical)), p.mouse.Wheel(true, int32(deltaHorizontal)))
|
||||||
|
}
|
||||||
|
|
@ -22,7 +22,7 @@
|
||||||
package inputcontrol
|
package inputcontrol
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"fmt"
|
||||||
"syscall"
|
"syscall"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
)
|
)
|
||||||
|
|
@ -98,7 +98,7 @@ func init() {
|
||||||
func InitWindowsController() (Controller, error) {
|
func InitWindowsController() (Controller, error) {
|
||||||
p := &windowsController{}
|
p := &windowsController{}
|
||||||
if err := sendInputProc.Find(); err != nil {
|
if err := sendInputProc.Find(); err != nil {
|
||||||
return nil, UnsupportedPlatformError{err}
|
return nil, &UnsupportedPlatformError{err}
|
||||||
}
|
}
|
||||||
return p, nil
|
return p, nil
|
||||||
}
|
}
|
||||||
|
|
@ -111,9 +111,8 @@ func (p *windowsController) sendInput(inputs []keybdInput) error {
|
||||||
if len(inputs) == 0 {
|
if len(inputs) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if r, _, err := sendInputProc.Call(uintptr(len(inputs)),
|
if sent, _, err := sendInputProc.Call(uintptr(len(inputs)),
|
||||||
uintptr(unsafe.Pointer(&inputs[0])),
|
uintptr(unsafe.Pointer(&inputs[0])), unsafe.Sizeof(inputs[0])); int(sent) != len(inputs) {
|
||||||
unsafe.Sizeof(inputs[0])); int(r) != len(inputs) {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -135,44 +134,45 @@ func (p *windowsController) KeyboardText(text string) error {
|
||||||
|
|
||||||
func (p *windowsController) KeyboardKey(key Key) error {
|
func (p *windowsController) KeyboardKey(key Key) error {
|
||||||
input := keybdInput{typ: inputKeyboard}
|
input := keybdInput{typ: inputKeyboard}
|
||||||
if key == KeyBackSpace {
|
switch key {
|
||||||
|
case KeyBackSpace:
|
||||||
input.wVk = vkBack
|
input.wVk = vkBack
|
||||||
} else if key == KeyReturn {
|
case KeyReturn:
|
||||||
input.wVk = vkReturn
|
input.wVk = vkReturn
|
||||||
} else if key == KeyEnd {
|
case KeyEnd:
|
||||||
input.wVk = vkEnd
|
input.wVk = vkEnd
|
||||||
} else if key == KeyHome {
|
case KeyHome:
|
||||||
input.wVk = vkHome
|
input.wVk = vkHome
|
||||||
} else if key == KeyLeft {
|
case KeyLeft:
|
||||||
input.wVk = vkLeft
|
input.wVk = vkLeft
|
||||||
} else if key == KeyUp {
|
case KeyUp:
|
||||||
input.wVk = vkUp
|
input.wVk = vkUp
|
||||||
} else if key == KeyRight {
|
case KeyRight:
|
||||||
input.wVk = vkRight
|
input.wVk = vkRight
|
||||||
} else if key == KeyDown {
|
case KeyDown:
|
||||||
input.wVk = vkDown
|
input.wVk = vkDown
|
||||||
} else if key == KeyDelete {
|
case KeyDelete:
|
||||||
input.wVk = vkDelete
|
input.wVk = vkDelete
|
||||||
} else if key == KeySuper {
|
case KeySuper:
|
||||||
input.wVk = vkLwin
|
input.wVk = vkLwin
|
||||||
} else if key == KeyBrowserBack {
|
case KeyBrowserBack:
|
||||||
input.wVk = vkBrowserBack
|
input.wVk = vkBrowserBack
|
||||||
} else if key == KeyBrowserForward {
|
case KeyBrowserForward:
|
||||||
input.wVk = vkBrowserForward
|
input.wVk = vkBrowserForward
|
||||||
} else if key == KeyVolumeMute {
|
case KeyVolumeMute:
|
||||||
input.wVk = vkVolumeMute
|
input.wVk = vkVolumeMute
|
||||||
} else if key == KeyVolumeDown {
|
case KeyVolumeDown:
|
||||||
input.wVk = vkVolumeDown
|
input.wVk = vkVolumeDown
|
||||||
} else if key == KeyVolumeUp {
|
case KeyVolumeUp:
|
||||||
input.wVk = vkVolumeUp
|
input.wVk = vkVolumeUp
|
||||||
} else if key == KeyMediaNextTrack {
|
case KeyMediaNextTrack:
|
||||||
input.wVk = vkMediaNextTrack
|
input.wVk = vkMediaNextTrack
|
||||||
} else if key == KeyMediaPrevTrack {
|
case KeyMediaPrevTrack:
|
||||||
input.wVk = vkMediaPrevTrack
|
input.wVk = vkMediaPrevTrack
|
||||||
} else if key == KeyMediaPlayPause {
|
case KeyMediaPlayPause:
|
||||||
input.wVk = vkMediaPlayPause
|
input.wVk = vkMediaPlayPause
|
||||||
} else {
|
default:
|
||||||
return errors.New("key not mapped to virtual-key code")
|
return fmt.Errorf("key not mapped to virtual-key code: %#v", key)
|
||||||
}
|
}
|
||||||
inputs := [...]keybdInput{input, input}
|
inputs := [...]keybdInput{input, input}
|
||||||
inputs[1].dwFlags |= keyeventfKeyup
|
inputs[1].dwFlags |= keyeventfKeyup
|
||||||
|
|
@ -194,10 +194,10 @@ func (p *windowsController) PointerButton(button PointerButton, press bool) erro
|
||||||
} else if button == PointerButtonRight {
|
} else if button == PointerButtonRight {
|
||||||
input.dwFlags = mouseeventfRightup
|
input.dwFlags = mouseeventfRightup
|
||||||
} else {
|
} else {
|
||||||
return errors.New("unsupported pointer button")
|
return fmt.Errorf("unsupported pointer button: %#v", button)
|
||||||
}
|
}
|
||||||
if r, _, err := sendInputProc.Call(1, uintptr(unsafe.Pointer(&input)),
|
if sent, _, err := sendInputProc.Call(1, uintptr(unsafe.Pointer(&input)),
|
||||||
unsafe.Sizeof(input)); int(r) != 1 {
|
unsafe.Sizeof(input)); int(sent) != 1 {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -210,8 +210,8 @@ func (p *windowsController) PointerMove(deltaX, deltaY int) error {
|
||||||
dy: int32(deltaY),
|
dy: int32(deltaY),
|
||||||
dwFlags: mouseeventfMove,
|
dwFlags: mouseeventfMove,
|
||||||
}
|
}
|
||||||
if r, _, err := sendInputProc.Call(1, uintptr(unsafe.Pointer(&input)),
|
if sent, _, err := sendInputProc.Call(1, uintptr(unsafe.Pointer(&input)),
|
||||||
unsafe.Sizeof(input)); int(r) != 1 {
|
unsafe.Sizeof(input)); int(sent) != 1 {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -236,9 +236,8 @@ func (p *windowsController) PointerScroll(deltaHorizontal, deltaVertical int, fi
|
||||||
if len(inputs) == 0 {
|
if len(inputs) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if r, _, err := sendInputProc.Call(uintptr(len(inputs)),
|
if sent, _, err := sendInputProc.Call(uintptr(len(inputs)),
|
||||||
uintptr(unsafe.Pointer(&inputs[0])),
|
uintptr(unsafe.Pointer(&inputs[0])), unsafe.Sizeof(inputs[0])); int(sent) != len(inputs) {
|
||||||
unsafe.Sizeof(inputs[0])); int(r) != len(inputs) {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -21,28 +21,42 @@
|
||||||
|
|
||||||
package inputcontrol
|
package inputcontrol
|
||||||
|
|
||||||
// #cgo LDFLAGS: -lX11 -lXtst
|
// #cgo LDFLAGS: -lX11 -lXrandr -lXtst
|
||||||
|
// #include <stdlib.h>
|
||||||
// #include <X11/Xlib.h>
|
// #include <X11/Xlib.h>
|
||||||
// #include <X11/Intrinsic.h>
|
// #include <X11/Intrinsic.h>
|
||||||
|
// #include <X11/extensions/Xrandr.h>
|
||||||
// #include <X11/extensions/XTest.h>
|
// #include <X11/extensions/XTest.h>
|
||||||
// #include <X11/XKBlib.h>
|
// #include <X11/XKBlib.h>
|
||||||
|
// Window MacroDefaultRootWindow(Display *dpy) {
|
||||||
|
// return DefaultRootWindow(dpy);
|
||||||
|
// }
|
||||||
import "C"
|
import "C"
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"maps"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
keyboardMappingDelay time.Duration = 500 * time.Millisecond
|
keyboardMappingDelay time.Duration = 200 * time.Millisecond
|
||||||
scrollDiv int = 20
|
scrollDiv int = 20
|
||||||
)
|
)
|
||||||
|
|
||||||
var modifierIndices [6]uint = [...]uint{C.ShiftMapIndex, C.Mod1MapIndex,
|
var modifierIndexes = [...]int{
|
||||||
C.Mod2MapIndex, C.Mod3MapIndex, C.Mod4MapIndex, C.Mod5MapIndex}
|
C.ShiftMapIndex,
|
||||||
|
C.Mod1MapIndex,
|
||||||
|
C.Mod2MapIndex,
|
||||||
|
C.Mod3MapIndex,
|
||||||
|
C.Mod4MapIndex,
|
||||||
|
C.Mod5MapIndex,
|
||||||
|
}
|
||||||
|
|
||||||
type x11Controller struct {
|
type x11Controller struct {
|
||||||
display *C.Display
|
display *C.Display
|
||||||
|
|
@ -55,17 +69,48 @@ func init() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func InitX11Controller() (Controller, error) {
|
func InitX11Controller() (Controller, error) {
|
||||||
sessionType := os.Getenv("XDG_SESSION_TYPE")
|
|
||||||
if sessionType != "" && sessionType != "x11" {
|
|
||||||
return nil, UnsupportedPlatformError{errors.New(fmt.Sprintf(
|
|
||||||
"unsupported session type '%v'", sessionType))}
|
|
||||||
}
|
|
||||||
display := C.XOpenDisplay(nil)
|
display := C.XOpenDisplay(nil)
|
||||||
if display == nil {
|
if display == nil {
|
||||||
return nil, UnsupportedPlatformError{
|
return nil, &UnsupportedPlatformError{
|
||||||
errors.New("failed to connect to X server")}
|
errors.New("failed to connect to X server"),
|
||||||
}
|
}
|
||||||
return &x11Controller{display: display}, nil
|
}
|
||||||
|
p := &x11Controller{display: display}
|
||||||
|
if p.xIsXwayland() {
|
||||||
|
p.Close()
|
||||||
|
return nil, &UnsupportedPlatformError{
|
||||||
|
errors.New("X server is Xwayland"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *x11Controller) xIsXwayland() bool {
|
||||||
|
// Detection method from https://gitlab.freedesktop.org/xorg/app/xisxwayland/-/blob/xisxwayland-2/xisxwayland.c
|
||||||
|
var opcode, event, error, major, minor C.int
|
||||||
|
xwaylandExtensionName := C.CString("XWAYLAND")
|
||||||
|
defer C.free(unsafe.Pointer(xwaylandExtensionName))
|
||||||
|
if C.XQueryExtension(p.display, xwaylandExtensionName, &opcode, &event, &error) != 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if C.XRRQueryExtension(p.display, &event, &error) == 0 ||
|
||||||
|
C.XRRQueryVersion(p.display, &major, &minor) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
resources := C.XRRGetScreenResourcesCurrent(p.display, C.MacroDefaultRootWindow(p.display))
|
||||||
|
if resources == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
defer C.XRRFreeScreenResources(resources)
|
||||||
|
if resources.noutput < 1 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
output := C.XRRGetOutputInfo(p.display, resources, *resources.outputs)
|
||||||
|
if output == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
defer C.XRRFreeOutputInfo(output)
|
||||||
|
return strings.HasPrefix(C.GoString(output.name), "XWAYLAND")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *x11Controller) Close() error {
|
func (p *x11Controller) Close() error {
|
||||||
|
|
@ -79,53 +124,68 @@ func (p *x11Controller) Close() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *x11Controller) findEmptyKeycodeLocked() (C.KeyCode, C.int, error) {
|
func (p *x11Controller) findEmptyKeycodeLocked() (keycode C.KeyCode, keysymsPerKeycode int, err error) {
|
||||||
var minKeycodes, maxKeycodes C.int
|
var minKeycodes, maxKeycodes C.int
|
||||||
C.XDisplayKeycodes(p.display, &minKeycodes, &maxKeycodes)
|
C.XDisplayKeycodes(p.display, &minKeycodes, &maxKeycodes)
|
||||||
var keysymsPerKeycode C.int
|
var keysymsPerKeycodeC C.int
|
||||||
keysyms := C.XGetKeyboardMapping(p.display, C.KeyCode(minKeycodes),
|
keysyms := C.XGetKeyboardMapping(
|
||||||
maxKeycodes-minKeycodes+1, &keysymsPerKeycode)
|
p.display,
|
||||||
|
C.KeyCode(minKeycodes),
|
||||||
|
maxKeycodes-minKeycodes+1,
|
||||||
|
&keysymsPerKeycodeC,
|
||||||
|
)
|
||||||
if keysyms == nil {
|
if keysyms == nil {
|
||||||
return 0, 0, errors.New("failed to get keyboard mapping")
|
return 0, 0, errors.New("failed to get keyboard mapping")
|
||||||
}
|
}
|
||||||
defer C.XFree(unsafe.Pointer(keysyms))
|
defer C.XFree(unsafe.Pointer(keysyms))
|
||||||
|
keysymsPerKeycode = int(keysymsPerKeycodeC)
|
||||||
keycodes:
|
keycodes:
|
||||||
for keycode := C.KeyCode(minKeycodes); keycode <= C.KeyCode(maxKeycodes); keycode++ {
|
for keycode := int(minKeycodes); keycode <= int(maxKeycodes); keycode++ {
|
||||||
for i := 0; i < int(keysymsPerKeycode); i++ {
|
for i := range keysymsPerKeycode {
|
||||||
keysymsIndex := int(keycode-
|
keysymsIndex := (keycode-int(minKeycodes))*keysymsPerKeycode + i
|
||||||
C.KeyCode(minKeycodes))*int(keysymsPerKeycode) + i
|
keysym := *(*C.KeySym)(unsafe.Pointer(
|
||||||
keysym := *(*C.KeySym)(unsafe.Pointer(uintptr(unsafe.Pointer(keysyms)) +
|
uintptr(unsafe.Pointer(keysyms)) +
|
||||||
uintptr(keysymsIndex)*unsafe.Sizeof(*keysyms)))
|
uintptr(keysymsIndex)*unsafe.Sizeof(*keysyms),
|
||||||
|
))
|
||||||
if keysym != 0 {
|
if keysym != 0 {
|
||||||
continue keycodes
|
continue keycodes
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return keycode, keysymsPerKeycode, nil
|
return C.KeyCode(keycode), keysymsPerKeycode, nil
|
||||||
}
|
}
|
||||||
return 0, 0, errors.New("no empty keycode found")
|
return 0, 0, errors.New("no empty keycode found")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *x11Controller) changeKeyMappingLocked(keysymsPerKeycode C.int,
|
func (p *x11Controller) changeKeyMappingLocked(
|
||||||
keycode C.KeyCode, keysym Keysym) {
|
keycode C.KeyCode, keysymsPerKeycode int, keysym Keysym,
|
||||||
|
) {
|
||||||
keycodeMapping := make([]C.KeySym, keysymsPerKeycode)
|
keycodeMapping := make([]C.KeySym, keysymsPerKeycode)
|
||||||
for i := range keycodeMapping {
|
for i := range keycodeMapping {
|
||||||
keycodeMapping[i] = C.KeySym(keysym)
|
keycodeMapping[i] = C.KeySym(keysym)
|
||||||
}
|
}
|
||||||
C.XChangeKeyboardMapping(p.display, C.int(keycode), keysymsPerKeycode,
|
C.XChangeKeyboardMapping(
|
||||||
(*C.KeySym)(unsafe.Pointer(&keycodeMapping[0])), 1)
|
p.display,
|
||||||
C.XFlush(p.display)
|
C.int(keycode),
|
||||||
|
C.int(keysymsPerKeycode),
|
||||||
|
(*C.KeySym)(unsafe.Pointer(&keycodeMapping[0])),
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
C.XSync(p.display, C.False)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *x11Controller) getModKeycodesLocked() map[uint]C.KeyCode {
|
func (p *x11Controller) getModKeycodesLocked() map[uint]C.KeyCode {
|
||||||
modKeymap := C.XGetModifierMapping(p.display)
|
modKeymap := C.XGetModifierMapping(p.display)
|
||||||
defer C.XFreeModifiermap(modKeymap)
|
defer C.XFreeModifiermap(modKeymap)
|
||||||
modKeycodes := make(map[uint]C.KeyCode)
|
modKeycodes := make(map[uint]C.KeyCode)
|
||||||
for _, modIndex := range modifierIndices {
|
for _, modIndex := range modifierIndexes {
|
||||||
for i := 0; i < int(modKeymap.max_keypermod); i++ {
|
for i := range int(modKeymap.max_keypermod) {
|
||||||
keycode := *(*C.KeyCode)(unsafe.Pointer(uintptr(unsafe.Pointer(modKeymap.modifiermap)) +
|
keycodeIndex := modIndex*int(modKeymap.max_keypermod) + i
|
||||||
uintptr(uint(modIndex)*uint(modKeymap.max_keypermod)+uint(i))))
|
keycode := *(*C.KeyCode)(unsafe.Pointer(
|
||||||
|
uintptr(unsafe.Pointer(modKeymap.modifiermap)) +
|
||||||
|
uintptr(keycodeIndex)*unsafe.Sizeof(*modKeymap.modifiermap),
|
||||||
|
))
|
||||||
if keycode != 0 {
|
if keycode != 0 {
|
||||||
modKeycodes[1<<uint(modIndex)] = keycode
|
modKeycodes[1<<modIndex] = keycode
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -133,50 +193,39 @@ func (p *x11Controller) getModKeycodesLocked() map[uint]C.KeyCode {
|
||||||
return modKeycodes
|
return modKeycodes
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *x11Controller) findKeycodeLocked(keyboard C.XkbDescPtr,
|
func (p *x11Controller) findKeycodeLocked(
|
||||||
modKeycodes map[uint]C.KeyCode, activeMods C.uint,
|
keyboard C.XkbDescPtr, modKeycodes map[uint]C.KeyCode, activeMods uint, keysym Keysym,
|
||||||
keysym Keysym) (C.KeyCode, C.uint) {
|
) (keycode C.KeyCode, mods uint) {
|
||||||
keycode := C.XKeysymToKeycode(p.display, C.KeySym(keysym))
|
keycode = C.XKeysymToKeycode(p.display, C.KeySym(keysym))
|
||||||
if keycode == 0 {
|
if keycode == 0 {
|
||||||
return 0, 0
|
return 0, 0
|
||||||
}
|
}
|
||||||
var alwaysActiveMods C.uint
|
for activeModBitmap := range uint(1) << len(modKeycodes) {
|
||||||
for modIndex := uint(0); modIndex < 8; modIndex++ {
|
mods := activeMods
|
||||||
mod := uint(1) << modIndex
|
for i, mod := range slices.Sorted(maps.Keys(modKeycodes)) {
|
||||||
if _, modAvailable := modKeycodes[mod]; !modAvailable {
|
if activeModBitmap&(1<<i) != 0 {
|
||||||
alwaysActiveMods |= activeMods & C.uint(mod)
|
mods ^= mod
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_, shiftModAvailable := modKeycodes[C.ShiftMask]
|
|
||||||
for _, modIndex := range modifierIndices {
|
|
||||||
var mod C.uint
|
|
||||||
if modIndex != C.ShiftMapIndex {
|
|
||||||
mod = 1 << modIndex
|
|
||||||
}
|
|
||||||
for _, shiftMod := range [...]C.uint{0, C.ShiftMask} {
|
|
||||||
if shiftMod != 0 && !shiftModAvailable {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
mods := alwaysActiveMods | shiftMod | mod
|
|
||||||
var retMods C.uint
|
var retMods C.uint
|
||||||
var retKeysym C.KeySym
|
var retKeysym C.KeySym
|
||||||
C.XkbTranslateKeyCode(keyboard, keycode, mods, &retMods, &retKeysym)
|
C.XkbTranslateKeyCode(keyboard, keycode, C.uint(mods), &retMods, &retKeysym)
|
||||||
if retKeysym == C.KeySym(keysym) {
|
if retKeysym == C.KeySym(keysym) {
|
||||||
return keycode, mods
|
return keycode, mods
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return 0, 0
|
return 0, 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *x11Controller) sendModsLocked(modKeycodes map[uint]C.KeyCode, mods C.uint,
|
func (p *x11Controller) sendModsLocked(
|
||||||
press bool) {
|
modKeycodes map[uint]C.KeyCode, mods uint, press bool,
|
||||||
|
) {
|
||||||
var pressC C.int = C.False
|
var pressC C.int = C.False
|
||||||
if press {
|
if press {
|
||||||
pressC = C.True
|
pressC = C.True
|
||||||
}
|
}
|
||||||
for mod, keycode := range modKeycodes {
|
for mod, keycode := range modKeycodes {
|
||||||
if mods&C.uint(mod) != 0 {
|
if mods&mod != 0 {
|
||||||
C.XTestFakeKeyEvent(p.display, C.uint(keycode), pressC, 0)
|
C.XTestFakeKeyEvent(p.display, C.uint(keycode), pressC, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -197,17 +246,22 @@ func (p *x11Controller) keyboardKeys(keys []Keysym) error {
|
||||||
C.XkbCompatMapMask|C.XkbGeometryMask, C.XkbUseCoreKbd)
|
C.XkbCompatMapMask|C.XkbGeometryMask, C.XkbUseCoreKbd)
|
||||||
defer C.XkbFreeKeyboard(keyboard, C.XkbAllComponentsMask, C.True)
|
defer C.XkbFreeKeyboard(keyboard, C.XkbAllComponentsMask, C.True)
|
||||||
var emptyKeycode C.KeyCode
|
var emptyKeycode C.KeyCode
|
||||||
var keysymsPerKeycode C.int
|
var keysymsPerKeycode int
|
||||||
for _, keysym := range keys {
|
for _, keysym := range keys {
|
||||||
var root, child C.Window
|
var root, child C.Window
|
||||||
var rootX, rootY, x, y C.int
|
var rootX, rootY, x, y C.int
|
||||||
var activeMods C.uint
|
var activeModsC C.uint
|
||||||
C.XSync(p.display, C.False)
|
C.XQueryPointer(
|
||||||
C.XQueryPointer(p.display, rootWindow, &root, &child, &rootX, &rootY,
|
p.display,
|
||||||
&x, &y, &activeMods)
|
rootWindow,
|
||||||
keycode, mods := p.findKeycodeLocked(keyboard, modKeycodes, activeMods,
|
&root, &child,
|
||||||
keysym)
|
&rootX, &rootY,
|
||||||
var pressMods, releaseMods C.uint
|
&x, &y,
|
||||||
|
&activeModsC,
|
||||||
|
)
|
||||||
|
activeMods := uint(activeModsC)
|
||||||
|
keycode, mods := p.findKeycodeLocked(keyboard, modKeycodes, activeMods, keysym)
|
||||||
|
var pressMods, releaseMods uint
|
||||||
if keycode == 0 {
|
if keycode == 0 {
|
||||||
if emptyKeycode == 0 {
|
if emptyKeycode == 0 {
|
||||||
var err error
|
var err error
|
||||||
|
|
@ -215,12 +269,10 @@ func (p *x11Controller) keyboardKeys(keys []Keysym) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer p.changeKeyMappingLocked(keysymsPerKeycode, emptyKeycode, 0)
|
defer p.changeKeyMappingLocked(emptyKeycode, keysymsPerKeycode, 0)
|
||||||
}
|
}
|
||||||
keycode = emptyKeycode
|
keycode = emptyKeycode
|
||||||
p.changeKeyMappingLocked(keysymsPerKeycode, keycode, keysym)
|
p.changeKeyMappingLocked(keycode, keysymsPerKeycode, keysym)
|
||||||
// race condition!
|
|
||||||
time.Sleep(keyboardMappingDelay)
|
|
||||||
} else {
|
} else {
|
||||||
pressMods = mods & ^activeMods
|
pressMods = mods & ^activeMods
|
||||||
releaseMods = activeMods & ^mods
|
releaseMods = activeMods & ^mods
|
||||||
|
|
@ -231,7 +283,7 @@ func (p *x11Controller) keyboardKeys(keys []Keysym) error {
|
||||||
C.XTestFakeKeyEvent(p.display, C.uint(keycode), C.False, 0)
|
C.XTestFakeKeyEvent(p.display, C.uint(keycode), C.False, 0)
|
||||||
p.sendModsLocked(modKeycodes, pressMods, false)
|
p.sendModsLocked(modKeycodes, pressMods, false)
|
||||||
p.sendModsLocked(modKeycodes, releaseMods, true)
|
p.sendModsLocked(modKeycodes, releaseMods, true)
|
||||||
C.XFlush(p.display)
|
C.XSync(p.display, C.False)
|
||||||
if keycode == emptyKeycode {
|
if keycode == emptyKeycode {
|
||||||
// race condition!
|
// race condition!
|
||||||
time.Sleep(keyboardMappingDelay)
|
time.Sleep(keyboardMappingDelay)
|
||||||
|
|
@ -275,21 +327,21 @@ func (p *x11Controller) sendButton(button uint, press bool) error {
|
||||||
pressC = C.True
|
pressC = C.True
|
||||||
}
|
}
|
||||||
C.XTestFakeButtonEvent(p.display, C.uint(button), pressC, 0)
|
C.XTestFakeButtonEvent(p.display, C.uint(button), pressC, 0)
|
||||||
C.XFlush(p.display)
|
C.XSync(p.display, C.False)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *x11Controller) PointerButton(button PointerButton, press bool) error {
|
func (p *x11Controller) PointerButton(button PointerButton, press bool) error {
|
||||||
if button == PointerButtonLeft {
|
switch button {
|
||||||
|
case PointerButtonLeft:
|
||||||
return p.sendButton(1, press)
|
return p.sendButton(1, press)
|
||||||
}
|
case PointerButtonRight:
|
||||||
if button == PointerButtonRight {
|
|
||||||
return p.sendButton(3, press)
|
return p.sendButton(3, press)
|
||||||
}
|
case PointerButtonMiddle:
|
||||||
if button == PointerButtonMiddle {
|
|
||||||
return p.sendButton(2, press)
|
return p.sendButton(2, press)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported pointer button: %#v", button)
|
||||||
}
|
}
|
||||||
return errors.New("unsupported pointer button")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *x11Controller) PointerMove(deltaX, deltaY int) error {
|
func (p *x11Controller) PointerMove(deltaX, deltaY int) error {
|
||||||
|
|
@ -299,7 +351,7 @@ func (p *x11Controller) PointerMove(deltaX, deltaY int) error {
|
||||||
return errors.New("X server connection closed")
|
return errors.New("X server connection closed")
|
||||||
}
|
}
|
||||||
C.XTestFakeRelativeMotionEvent(p.display, C.int(deltaX), C.int(deltaY), 0)
|
C.XTestFakeRelativeMotionEvent(p.display, C.int(deltaX), C.int(deltaY), 0)
|
||||||
C.XFlush(p.display)
|
C.XSync(p.display, C.False)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -320,7 +372,7 @@ func (p *x11Controller) PointerScroll(deltaHorizontal, deltaVertical int, finish
|
||||||
buttonHorizontal = 6
|
buttonHorizontal = 6
|
||||||
stepsHorizontal = -stepsHorizontal
|
stepsHorizontal = -stepsHorizontal
|
||||||
}
|
}
|
||||||
for i := 0; i < stepsHorizontal; i++ {
|
for range stepsHorizontal {
|
||||||
if err := p.sendButton(buttonHorizontal, true); err != nil {
|
if err := p.sendButton(buttonHorizontal, true); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -333,7 +385,7 @@ func (p *x11Controller) PointerScroll(deltaHorizontal, deltaVertical int, finish
|
||||||
buttonVertical = 4
|
buttonVertical = 4
|
||||||
stepsVertical = -stepsVertical
|
stepsVertical = -stepsVertical
|
||||||
}
|
}
|
||||||
for i := 0; i < stepsVertical; i++ {
|
for range stepsVertical {
|
||||||
if err := p.sendButton(buttonVertical, true); err != nil {
|
if err := p.sendButton(buttonVertical, true); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
130
inputcontrol/keymap.go
Normal file
130
inputcontrol/keymap.go
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
//go:build uinput
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 Unrud <unrud@outlook.com>
|
||||||
|
*
|
||||||
|
* This file is part of Remote-Touchpad.
|
||||||
|
*
|
||||||
|
* Remote-Touchpad is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Remote-Touchpad is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Remote-Touchpad. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package inputcontrol
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
const bkeymapFileSignature = "bkeymap"
|
||||||
|
|
||||||
|
// uapi/linux/keyboard.h
|
||||||
|
const (
|
||||||
|
nrKeys = 128
|
||||||
|
maxNrKeymaps = 256
|
||||||
|
nrShift = 9
|
||||||
|
|
||||||
|
ktLatin = 0
|
||||||
|
ktShift = 7
|
||||||
|
ktLetter = 11
|
||||||
|
)
|
||||||
|
|
||||||
|
type KeyCombo struct {
|
||||||
|
Key int
|
||||||
|
ShiftKeys [nrShift]int
|
||||||
|
}
|
||||||
|
|
||||||
|
type Keymap struct {
|
||||||
|
keyComboMap map[rune]KeyCombo
|
||||||
|
}
|
||||||
|
|
||||||
|
func (keymap Keymap) Get(character rune) (keyCombo KeyCombo, found bool) {
|
||||||
|
keyCombo, found = keymap.keyComboMap[character]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadKeymap(name string) (*Keymap, error) {
|
||||||
|
if len(name) == 0 || strings.HasPrefix(name, "-") {
|
||||||
|
return nil, fmt.Errorf("invalid keymap name: %#v", name)
|
||||||
|
}
|
||||||
|
bkeymap, err := exec.Command("loadkeys", "--bkeymap", name).Output()
|
||||||
|
if err != nil {
|
||||||
|
return nil, &UnsupportedPlatformError{err}
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(string(bkeymap), bkeymapFileSignature) {
|
||||||
|
return nil, fmt.Errorf("invalid bkeymap: signature not found")
|
||||||
|
}
|
||||||
|
bkeymapHeader := bkeymap[len(bkeymapFileSignature):]
|
||||||
|
var keymaps [][nrKeys]int
|
||||||
|
if len(bkeymapHeader) < maxNrKeymaps {
|
||||||
|
return nil, fmt.Errorf("invalid bkeymap: EOF")
|
||||||
|
}
|
||||||
|
currentBkeymap := bkeymapHeader[maxNrKeymaps:]
|
||||||
|
for keymapIndex := 0; keymapIndex < maxNrKeymaps; keymapIndex += 1 {
|
||||||
|
if bkeymapHeader[keymapIndex] == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if len(currentBkeymap) < nrKeys*2 {
|
||||||
|
return nil, fmt.Errorf("invalid bkeymap: EOF")
|
||||||
|
}
|
||||||
|
for len(keymaps) <= keymapIndex {
|
||||||
|
keymaps = append(keymaps, [nrKeys]int{})
|
||||||
|
}
|
||||||
|
for key := 0; key < nrKeys; key += 1 {
|
||||||
|
var keysym uint16
|
||||||
|
copy(unsafe.Slice((*byte)(unsafe.Pointer(&keysym)), 2),
|
||||||
|
currentBkeymap[key*2:])
|
||||||
|
keymaps[keymapIndex][key] = int(keysym)
|
||||||
|
}
|
||||||
|
currentBkeymap = currentBkeymap[nrKeys*2:]
|
||||||
|
}
|
||||||
|
keymap := Keymap{keyComboMap: make(map[rune]KeyCombo)}
|
||||||
|
if len(keymaps) == 0 {
|
||||||
|
return &keymap, nil
|
||||||
|
}
|
||||||
|
var shiftKeys [nrShift]int
|
||||||
|
for key, keysym := range keymaps[0] {
|
||||||
|
keyType := keysym >> 8
|
||||||
|
keyValue := keysym & 0xff
|
||||||
|
if keyType == ktShift && keyValue < nrShift && shiftKeys[keyValue] == 0 {
|
||||||
|
shiftKeys[keyValue] = key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
keymapLoop:
|
||||||
|
for keymapIndex := range keymaps {
|
||||||
|
var activeShiftKeys [nrShift]int
|
||||||
|
for i, shiftKey := range shiftKeys {
|
||||||
|
if keymapIndex&(1<<i) != 0 {
|
||||||
|
if shiftKey == 0 {
|
||||||
|
continue keymapLoop
|
||||||
|
}
|
||||||
|
activeShiftKeys[i] = shiftKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for key, keysym := range keymaps[keymapIndex] {
|
||||||
|
keyType := keysym >> 8
|
||||||
|
keyValue := keysym & 0xff
|
||||||
|
if (keyType != ktLatin && keyType != ktLetter) || keyValue == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
character := rune(keyValue)
|
||||||
|
if _, ok := keymap.keyComboMap[character]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
keymap.keyComboMap[character] = KeyCombo{key, activeShiftKeys}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &keymap, nil
|
||||||
|
}
|
||||||
|
|
@ -23,7 +23,7 @@ package inputcontrol
|
||||||
|
|
||||||
//go:generate go run keysyms.generator.go
|
//go:generate go run keysyms.generator.go
|
||||||
|
|
||||||
import "errors"
|
import "fmt"
|
||||||
|
|
||||||
type Keysym int32
|
type Keysym int32
|
||||||
|
|
||||||
|
|
@ -57,8 +57,8 @@ func RuneToKeysym(runeValue rune) (Keysym, error) {
|
||||||
keysym, found := keysymsMap[runeValue]
|
keysym, found := keysymsMap[runeValue]
|
||||||
if !found {
|
if !found {
|
||||||
if runeValue < 0x100 || runeValue > 0x10ffff {
|
if runeValue < 0x100 || runeValue > 0x10ffff {
|
||||||
return 0, errors.New("rune not mappend to keysym and " +
|
return 0, fmt.Errorf("rune not mappend to keysym and "+
|
||||||
"out of range for direct unicode mapping")
|
"out of range for direct unicode mapping: %q", runeValue)
|
||||||
}
|
}
|
||||||
keysym = Keysym(0x01000000 + runeValue)
|
keysym = Keysym(0x01000000 + runeValue)
|
||||||
}
|
}
|
||||||
|
|
@ -66,59 +66,44 @@ func RuneToKeysym(runeValue rune) (Keysym, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func KeyToKeysym(key Key) (Keysym, error) {
|
func KeyToKeysym(key Key) (Keysym, error) {
|
||||||
if key == KeyBackSpace {
|
switch key {
|
||||||
|
case KeyBackSpace:
|
||||||
return xkBackSpace, nil
|
return xkBackSpace, nil
|
||||||
}
|
case KeyReturn:
|
||||||
if key == KeyReturn {
|
|
||||||
return xkReturn, nil
|
return xkReturn, nil
|
||||||
}
|
case KeyDelete:
|
||||||
if key == KeyDelete {
|
|
||||||
return xkDelete, nil
|
return xkDelete, nil
|
||||||
}
|
case KeyHome:
|
||||||
if key == KeyHome {
|
|
||||||
return xkHome, nil
|
return xkHome, nil
|
||||||
}
|
case KeyLeft:
|
||||||
if key == KeyLeft {
|
|
||||||
return xkLeft, nil
|
return xkLeft, nil
|
||||||
}
|
case KeyUp:
|
||||||
if key == KeyUp {
|
|
||||||
return xkUp, nil
|
return xkUp, nil
|
||||||
}
|
case KeyRight:
|
||||||
if key == KeyRight {
|
|
||||||
return xkRight, nil
|
return xkRight, nil
|
||||||
}
|
case KeyDown:
|
||||||
if key == KeyDown {
|
|
||||||
return xkDown, nil
|
return xkDown, nil
|
||||||
}
|
case KeyEnd:
|
||||||
if key == KeyEnd {
|
|
||||||
return xkEnd, nil
|
return xkEnd, nil
|
||||||
}
|
case KeySuper:
|
||||||
if key == KeySuper {
|
|
||||||
return xkSuperL, nil
|
return xkSuperL, nil
|
||||||
}
|
case KeyVolumeMute:
|
||||||
if key == KeyVolumeMute {
|
|
||||||
return xf86xkAudioMute, nil
|
return xf86xkAudioMute, nil
|
||||||
}
|
case KeyVolumeDown:
|
||||||
if key == KeyVolumeDown {
|
|
||||||
return xf86xkAudioLowerVolume, nil
|
return xf86xkAudioLowerVolume, nil
|
||||||
}
|
case KeyVolumeUp:
|
||||||
if key == KeyVolumeUp {
|
|
||||||
return xf86xkAudioRaiseVolume, nil
|
return xf86xkAudioRaiseVolume, nil
|
||||||
}
|
case KeyMediaPlayPause:
|
||||||
if key == KeyMediaPlayPause {
|
|
||||||
return xf86xkAudioPlay, nil
|
return xf86xkAudioPlay, nil
|
||||||
}
|
case KeyMediaPrevTrack:
|
||||||
if key == KeyMediaPrevTrack {
|
|
||||||
return xf86xkAudioPrev, nil
|
return xf86xkAudioPrev, nil
|
||||||
}
|
case KeyMediaNextTrack:
|
||||||
if key == KeyMediaNextTrack {
|
|
||||||
return xf86xkAudioNext, nil
|
return xf86xkAudioNext, nil
|
||||||
}
|
case KeyBrowserBack:
|
||||||
if key == KeyBrowserBack {
|
|
||||||
return xf86xkBack, nil
|
return xf86xkBack, nil
|
||||||
}
|
case KeyBrowserForward:
|
||||||
if key == KeyBrowserForward {
|
|
||||||
return xf86xkForward, nil
|
return xf86xkForward, nil
|
||||||
|
default:
|
||||||
|
return 0, fmt.Errorf("key not mapped to keysym: %#v", key)
|
||||||
}
|
}
|
||||||
return 0, errors.New("key not mapped to keysym")
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
25
main.go
25
main.go
|
|
@ -27,9 +27,6 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/unrud/remote-touchpad/inputcontrol"
|
|
||||||
"github.com/unrud/remote-touchpad/terminal"
|
|
||||||
"golang.org/x/net/websocket"
|
|
||||||
"log"
|
"log"
|
||||||
mathrand "math/rand"
|
mathrand "math/rand"
|
||||||
"net"
|
"net"
|
||||||
|
|
@ -39,6 +36,10 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"github.com/unrud/remote-touchpad/inputcontrol"
|
||||||
|
"github.com/unrud/remote-touchpad/terminal"
|
||||||
|
"golang.org/x/net/websocket"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
@ -47,7 +48,7 @@ const (
|
||||||
authenticationRateBurst int = 10
|
authenticationRateBurst int = 10
|
||||||
challengeLength int = 8
|
challengeLength int = 8
|
||||||
defaultBind string = ":0"
|
defaultBind string = ":0"
|
||||||
version string = "1.2.1"
|
version string = "1.5.4"
|
||||||
prettyAppName string = "Remote Touchpad"
|
prettyAppName string = "Remote Touchpad"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -184,21 +185,25 @@ func main() {
|
||||||
}
|
}
|
||||||
var controller inputcontrol.Controller
|
var controller inputcontrol.Controller
|
||||||
var controllerName string
|
var controllerName string
|
||||||
platformErrors := ""
|
var platformErrs []error
|
||||||
for _, controllerInfo := range inputcontrol.Controllers {
|
for _, controllerInfo := range inputcontrol.Controllers {
|
||||||
controllerName = controllerInfo.Name
|
controllerName = controllerInfo.Name
|
||||||
var err error
|
var err error
|
||||||
controller, err = controllerInfo.Init()
|
controller, err = controllerInfo.Init()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
break
|
break
|
||||||
} else if _, ok := err.(inputcontrol.UnsupportedPlatformError); ok {
|
|
||||||
platformErrors += fmt.Sprintf("%s controller: %v\n", controllerName, err)
|
|
||||||
} else {
|
} else {
|
||||||
log.Fatalf("%s controller: %v", controllerName, err)
|
var unsupportedErr *inputcontrol.UnsupportedPlatformError
|
||||||
|
wrappedErr := fmt.Errorf("%v controller: %w", controllerName, err)
|
||||||
|
if errors.As(err, &unsupportedErr) {
|
||||||
|
platformErrs = append(platformErrs, wrappedErr)
|
||||||
|
} else {
|
||||||
|
log.Fatal(wrappedErr)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if controller == nil {
|
if controller == nil {
|
||||||
log.Fatal("unsupported platform:\n" + platformErrors)
|
log.Fatal(fmt.Errorf("unsupported platform:\n%w", errors.Join(platformErrs...)))
|
||||||
}
|
}
|
||||||
defer controller.Close()
|
defer controller.Close()
|
||||||
authenticationChallenges := make(chan challenge, authenticationRateBurst)
|
authenticationChallenges := make(chan challenge, authenticationRateBurst)
|
||||||
|
|
@ -241,7 +246,7 @@ func main() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := processCommand(controller, message); err != nil {
|
if err := processCommand(controller, message); err != nil {
|
||||||
log.Printf("%s controller: %v", controllerName, err)
|
log.Print(fmt.Errorf("%s controller: %w", controllerName, err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
12
remote-touchpad.service
Normal file
12
remote-touchpad.service
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
[Unit]
|
||||||
|
Description=Remote Touchpad Service
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=%h/.local/bin/remote-touchpad
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5s
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
149
rts
Executable file
149
rts
Executable file
|
|
@ -0,0 +1,149 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Remote Touchpad CLI Wrapper
|
||||||
|
# Provides easy access to remote-touchpad service
|
||||||
|
|
||||||
|
SERVICE_NAME="remote-touchpad"
|
||||||
|
SERVICE_USER="$USER"
|
||||||
|
|
||||||
|
install() {
|
||||||
|
echo "Installing remote-touchpad as a user systemd service..."
|
||||||
|
|
||||||
|
# Check if Go is installed
|
||||||
|
if ! command -v go &> /dev/null; then
|
||||||
|
echo "Error: Go is not installed. Please install Go first."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Building remote-touchpad binary..."
|
||||||
|
go build -tags portal,uinput,x11 -o remote-touchpad
|
||||||
|
|
||||||
|
if [ ! -f "remote-touchpad" ]; then
|
||||||
|
echo "Error: Failed to build remote-touchpad binary"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Setup directories
|
||||||
|
mkdir -p ~/.local/bin ~/.config/systemd/user
|
||||||
|
|
||||||
|
echo "Installing service files..."
|
||||||
|
cp remote-touchpad ~/.local/bin/ || echo "Error: cp to ~/.local/bin failed"
|
||||||
|
cp remote-touchpad.service ~/.config/systemd/user/ || echo "Error: cp to ~/.config/systemd/user failed"
|
||||||
|
|
||||||
|
echo "Enabling and starting service..."
|
||||||
|
systemctl --user daemon-reload
|
||||||
|
systemctl --user enable --now ${SERVICE_NAME}.service
|
||||||
|
|
||||||
|
echo "✓ remote-touchpad user service installed and started"
|
||||||
|
echo ""
|
||||||
|
echo "Use './rts status' to check the service"
|
||||||
|
echo "Use './rts url' to get the connection URL"
|
||||||
|
}
|
||||||
|
|
||||||
|
start() {
|
||||||
|
systemctl --user start ${SERVICE_NAME}.service
|
||||||
|
echo "Started remote-touchpad service"
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
systemctl --user stop ${SERVICE_NAME}.service
|
||||||
|
echo "Stopped remote-touchpad service"
|
||||||
|
}
|
||||||
|
|
||||||
|
status() {
|
||||||
|
systemctl --user status ${SERVICE_NAME}.service --no-pager
|
||||||
|
}
|
||||||
|
|
||||||
|
restart() {
|
||||||
|
systemctl --user restart ${SERVICE_NAME}.service
|
||||||
|
echo "Restarted remote-touchpad service"
|
||||||
|
}
|
||||||
|
|
||||||
|
url() {
|
||||||
|
# Extract URL from service logs
|
||||||
|
echo "Fetching remote-touchpad URL..."
|
||||||
|
local url=$(journalctl --user -u ${SERVICE_NAME}.service -n 100 --no-pager 2>/dev/null | grep -oE "http://[^ ]+|https://[^ ]+" | tail -1)
|
||||||
|
if [ -n "$url" ]; then
|
||||||
|
echo "Remote Touchpad URL: $url"
|
||||||
|
# Generate QR code if qrencode is available
|
||||||
|
if command -v qrencode &> /dev/null; then
|
||||||
|
echo "Generating QR code..."
|
||||||
|
echo "$url" | qrencode -t ansiutf8
|
||||||
|
else
|
||||||
|
echo "QR code generation requires qrencode. Install it with:"
|
||||||
|
echo " sudo apt-get install qrencode # Debian/Ubuntu"
|
||||||
|
echo " sudo dnf install qrencode # Fedora"
|
||||||
|
echo " brew install qrencode # macOS"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Could not find URL in service logs. Is the service running?"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
log() {
|
||||||
|
echo "Viewing remote-touchpad service logs..."
|
||||||
|
journalctl --user -u ${SERVICE_NAME}.service --no-pager
|
||||||
|
}
|
||||||
|
|
||||||
|
uninstall() {
|
||||||
|
echo "Uninstalling remote-touchpad..."
|
||||||
|
systemctl --user stop ${SERVICE_NAME}.service
|
||||||
|
systemctl --user disable ${SERVICE_NAME}.service
|
||||||
|
rm -f ~/.config/systemd/user/${SERVICE_NAME}.service
|
||||||
|
rm -f ~/.local/bin/remote-touchpad
|
||||||
|
systemctl --user daemon-reload
|
||||||
|
echo "✓ remote-touchpad uninstalled"
|
||||||
|
}
|
||||||
|
|
||||||
|
help() {
|
||||||
|
echo "Remote Touchpad CLI Wrapper"
|
||||||
|
echo ""
|
||||||
|
echo "Usage: rts [command]"
|
||||||
|
echo ""
|
||||||
|
echo "Commands:"
|
||||||
|
echo " install - Install remote-touchpad as a systemd user service"
|
||||||
|
echo " start - Start the remote-touchpad service"
|
||||||
|
echo " stop - Stop the remote-touchpad service"
|
||||||
|
echo " status - Check service status"
|
||||||
|
echo " restart - Restart the service"
|
||||||
|
echo " url - Show the remote-touchpad URL"
|
||||||
|
echo " log - View service logs"
|
||||||
|
echo " uninstall - Uninstall the service"
|
||||||
|
echo " help - Show this help message"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main command handler
|
||||||
|
case "$1" in
|
||||||
|
install)
|
||||||
|
install
|
||||||
|
;;
|
||||||
|
start)
|
||||||
|
start
|
||||||
|
;;
|
||||||
|
stop)
|
||||||
|
stop
|
||||||
|
;;
|
||||||
|
status)
|
||||||
|
status
|
||||||
|
;;
|
||||||
|
restart)
|
||||||
|
restart
|
||||||
|
;;
|
||||||
|
url)
|
||||||
|
url
|
||||||
|
;;
|
||||||
|
log)
|
||||||
|
log
|
||||||
|
;;
|
||||||
|
uninstall)
|
||||||
|
uninstall
|
||||||
|
;;
|
||||||
|
help|--help|-h|"")
|
||||||
|
help
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown command: $1"
|
||||||
|
help
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
@ -1,17 +1,21 @@
|
||||||
# Build by running "snapcraft".
|
# Build by running `snapcraft`.
|
||||||
|
|
||||||
# WARNING:
|
# WARNING:
|
||||||
# Snapcraft uses caching for already build steps but it's buggy and can cause strange problems.
|
# Snapcraft uses caching for already build steps but it's buggy and can cause strange problems.
|
||||||
# Clean the cache by running "snapcraft clean".
|
# Clean the cache by running `snapcraft clean`.
|
||||||
|
|
||||||
name: remote-touchpad
|
name: remote-touchpad
|
||||||
license: GPL-3.0+
|
license: GPL-3.0+
|
||||||
grade: stable
|
grade: stable
|
||||||
adopt-info: remote-touchpad
|
adopt-info: remote-touchpad
|
||||||
|
|
||||||
base: core18
|
base: core24
|
||||||
confinement: strict
|
confinement: strict
|
||||||
|
|
||||||
|
platforms:
|
||||||
|
amd64:
|
||||||
|
arm64:
|
||||||
|
|
||||||
apps:
|
apps:
|
||||||
remote-touchpad:
|
remote-touchpad:
|
||||||
command: bin/remote-touchpad
|
command: bin/remote-touchpad
|
||||||
|
|
@ -20,13 +24,13 @@ apps:
|
||||||
- desktop
|
- desktop
|
||||||
- x11
|
- x11
|
||||||
remote-touchpad-wait-on-error:
|
remote-touchpad-wait-on-error:
|
||||||
command: usr/bin/remote-touchpad-wait-on-error
|
command: bin/remote-touchpad-wait-on-error
|
||||||
plugs:
|
plugs:
|
||||||
- network-bind
|
- network-bind
|
||||||
- desktop
|
- desktop
|
||||||
- x11
|
- x11
|
||||||
common-id: com.github.unrud.RemoteTouchpad
|
common-id: com.github.unrud.RemoteTouchpad
|
||||||
desktop: share/applications/com.github.unrud.RemoteTouchpad.desktop
|
desktop: usr/share/applications/com.github.unrud.RemoteTouchpad.desktop
|
||||||
|
|
||||||
parts:
|
parts:
|
||||||
remote-touchpad:
|
remote-touchpad:
|
||||||
|
|
@ -36,18 +40,22 @@ parts:
|
||||||
- x11
|
- x11
|
||||||
source: .
|
source: .
|
||||||
source-type: git
|
source-type: git
|
||||||
|
build-snaps:
|
||||||
|
- go
|
||||||
build-packages:
|
build-packages:
|
||||||
- gcc
|
- gcc
|
||||||
- libc6-dev
|
- libc6-dev
|
||||||
- libxt-dev
|
- libxt-dev
|
||||||
|
- libxrandr-dev
|
||||||
- libxtst-dev
|
- libxtst-dev
|
||||||
stage-packages:
|
stage-packages:
|
||||||
- libxt6
|
- libxt6
|
||||||
|
- libxrandr2
|
||||||
- libxtst6
|
- libxtst6
|
||||||
override-build: |
|
override-build: |
|
||||||
snapcraftctl build
|
craftctl default
|
||||||
install -Dm0755 -t "${SNAPCRAFT_PART_INSTALL}/usr/bin" desktop/remote-touchpad-wait-on-error
|
install -Dm0755 -t "${CRAFT_PART_INSTALL}/bin" desktop/remote-touchpad-wait-on-error
|
||||||
install -Dm0644 -t "${SNAPCRAFT_PART_INSTALL}/usr/share/metainfo" desktop/com.github.unrud.RemoteTouchpad.metainfo.xml
|
install -Dm0644 -t "${CRAFT_PART_INSTALL}/usr/share/metainfo" desktop/com.github.unrud.RemoteTouchpad.metainfo.xml
|
||||||
install -Dm0644 -t "${SNAPCRAFT_PART_INSTALL}/usr/share/applications" desktop/com.github.unrud.RemoteTouchpad.desktop
|
install -Dm0644 -t "${CRAFT_PART_INSTALL}/usr/share/applications" desktop/com.github.unrud.RemoteTouchpad.desktop
|
||||||
install -Dm0644 -t "${SNAPCRAFT_PART_INSTALL}/usr/share/icons/hicolor/scalable/apps" desktop/com.github.unrud.RemoteTouchpad.svg
|
install -Dm0644 -t "${CRAFT_PART_INSTALL}/usr/share/icons/hicolor/scalable/apps" desktop/com.github.unrud.RemoteTouchpad.svg
|
||||||
parse-info: [usr/share/metainfo/com.github.unrud.RemoteTouchpad.metainfo.xml]
|
parse-info: [usr/share/metainfo/com.github.unrud.RemoteTouchpad.metainfo.xml]
|
||||||
|
|
|
||||||
33
webdata.go
33
webdata.go
|
|
@ -1,19 +1,52 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 Unrud <unrud@outlook.com>
|
||||||
|
*
|
||||||
|
* This file is part of Remote-Touchpad.
|
||||||
|
*
|
||||||
|
* Remote-Touchpad is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Remote-Touchpad is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Remote-Touchpad. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"embed"
|
"embed"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"log"
|
"log"
|
||||||
|
"mime"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed webdata/*
|
//go:embed webdata/*
|
||||||
var webdataFSWithPrefix embed.FS
|
var webdataFSWithPrefix embed.FS
|
||||||
var webdataFS fs.FS
|
var webdataFS fs.FS
|
||||||
|
|
||||||
|
var webdataTypes = map[string]string{
|
||||||
|
".css": "text/css; charset=utf-8",
|
||||||
|
".html": "text/html; charset=utf-8",
|
||||||
|
".mjs": "text/javascript; charset=utf-8",
|
||||||
|
".png": "image/png",
|
||||||
|
".woff": "font/woff",
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
var err error
|
var err error
|
||||||
webdataFS, err = fs.Sub(webdataFSWithPrefix, "webdata")
|
webdataFS, err = fs.Sub(webdataFSWithPrefix, "webdata")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
for ext, typ := range webdataTypes {
|
||||||
|
if err := mime.AddExtensionType(ext, typ); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
82
webdata/app/compat.mjs
Normal file
82
webdata/app/compat.mjs
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2018-2019, 2023 Unrud <unrud@outlook.com>
|
||||||
|
*
|
||||||
|
* This file is part of Remote-Touchpad.
|
||||||
|
*
|
||||||
|
* Remote-Touchpad is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Remote-Touchpad is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Remote-Touchpad. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const fullscreenEnabled = () => {
|
||||||
|
return (document.fullscreenEnabled ||
|
||||||
|
document.webkitFullscreenEnabled ||
|
||||||
|
false);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const requestFullscreen = (element, options) => {
|
||||||
|
if (element.requestFullscreen) {
|
||||||
|
element.requestFullscreen(options);
|
||||||
|
} else if (element.webkitRequestFullscreen) {
|
||||||
|
element.webkitRequestFullscreen(options);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const exitFullscreen = () => {
|
||||||
|
if (document.exitFullscreen) {
|
||||||
|
document.exitFullscreen();
|
||||||
|
} else if (document.webkitExitFullscreen) {
|
||||||
|
document.webkitExitFullscreen();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fullscreenElement = () => {
|
||||||
|
return (document.fullscreenElement ||
|
||||||
|
document.webkitFullscreenElement ||
|
||||||
|
null);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const addFullscreenchangeEventListener = (listener) => {
|
||||||
|
if ("onfullscreenchange" in document) {
|
||||||
|
document.addEventListener("fullscreenchange", listener);
|
||||||
|
} else if ("onwebkitfullscreenchange" in document) {
|
||||||
|
document.addEventListener("webkitfullscreenchange", listener);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const requestPointerLock = (element) => {
|
||||||
|
if (element.requestPointerLock) {
|
||||||
|
element.requestPointerLock();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const exitPointerLock = () => {
|
||||||
|
if (document.exitPointerLock) {
|
||||||
|
document.exitPointerLock();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const pointerLockElement = () => {
|
||||||
|
return document.pointerLockElement || null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const addPointerlockchangeEventListener = (listener) => {
|
||||||
|
if ("onpointerlockchange" in document) {
|
||||||
|
document.addEventListener("pointerlockchange", listener);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const vibrate = (pattern) => {
|
||||||
|
if (navigator.vibrate) {
|
||||||
|
navigator.vibrate(pattern);
|
||||||
|
}
|
||||||
|
};
|
||||||
121
webdata/app/inputcontroller.mjs
Normal file
121
webdata/app/inputcontroller.mjs
Normal file
|
|
@ -0,0 +1,121 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2018-2019, 2023 Unrud <unrud@outlook.com>
|
||||||
|
*
|
||||||
|
* This file is part of Remote-Touchpad.
|
||||||
|
*
|
||||||
|
* Remote-Touchpad is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Remote-Touchpad is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Remote-Touchpad. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const POINTER_BUTTON_LEFT = 0;
|
||||||
|
export const POINTER_BUTTON_RIGHT = 1;
|
||||||
|
export const POINTER_BUTTON_MIDDLE = 2;
|
||||||
|
|
||||||
|
export const KEY_VOLUME_MUTE = 0;
|
||||||
|
export const KEY_VOLUME_DOWN = 1;
|
||||||
|
export const KEY_VOLUME_UP = 2;
|
||||||
|
export const KEY_MEDIA_PLAY_PAUSE = 3;
|
||||||
|
export const KEY_MEDIA_PREV_TRACK = 4;
|
||||||
|
export const KEY_MEDIA_NEXT_TRACK = 5;
|
||||||
|
export const KEY_BROWSER_BACK = 6;
|
||||||
|
export const KEY_BROWSER_FORWARD = 7;
|
||||||
|
export const KEY_SUPER = 8;
|
||||||
|
export const KEY_LEFT = 9;
|
||||||
|
export const KEY_RIGHT = 10;
|
||||||
|
export const KEY_UP = 11;
|
||||||
|
export const KEY_DOWN = 12;
|
||||||
|
export const KEY_HOME = 13;
|
||||||
|
export const KEY_END = 14;
|
||||||
|
export const KEY_BACK_SPACE = 15;
|
||||||
|
export const KEY_DELETE = 16;
|
||||||
|
export const KEY_RETURN = 17;
|
||||||
|
|
||||||
|
export default class InputController {
|
||||||
|
#updateRate = 0;
|
||||||
|
|
||||||
|
#moveXSum = 0;
|
||||||
|
#moveYSum = 0;
|
||||||
|
#scrollHSum = 0;
|
||||||
|
#scrollVSum = 0;
|
||||||
|
#scrolling = false;
|
||||||
|
#scrollFinish = false;
|
||||||
|
#updateTimeoutActive = false;
|
||||||
|
#socket;
|
||||||
|
|
||||||
|
constructor(socket) {
|
||||||
|
this.#socket = socket;
|
||||||
|
}
|
||||||
|
|
||||||
|
configure(config) {
|
||||||
|
this.#updateRate = config.updateRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
#startUpdate(fromTimeout) {
|
||||||
|
if (this.#updateTimeoutActive && !fromTimeout) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.#updateTimeoutActive = false;
|
||||||
|
let finished = true;
|
||||||
|
const xInt = Math.trunc(this.#moveXSum);
|
||||||
|
const yInt = Math.trunc(this.#moveYSum);
|
||||||
|
if (xInt != 0 || yInt != 0) {
|
||||||
|
this.#socket.send("m" + xInt + ";" + yInt);
|
||||||
|
this.#moveXSum -= xInt;
|
||||||
|
this.#moveYSum -= yInt;
|
||||||
|
finished = false;
|
||||||
|
}
|
||||||
|
const hInt = Math.trunc(this.#scrollHSum);
|
||||||
|
const vInt = Math.trunc(this.#scrollVSum);
|
||||||
|
if (hInt != 0 || vInt != 0) {
|
||||||
|
this.#socket.send((this.#scrollFinish ? "S" : "s") + hInt + ";" + vInt);
|
||||||
|
this.#scrollHSum -= hInt;
|
||||||
|
this.#scrollVSum -= vInt;
|
||||||
|
this.#scrolling = !this.#scrollFinish;
|
||||||
|
this.#scrollFinish = false;
|
||||||
|
finished = false;
|
||||||
|
} else if (this.#scrollFinish && this.#scrolling) {
|
||||||
|
this.#socket.send("S");
|
||||||
|
this.#scrolling = false;
|
||||||
|
this.#scrollFinish = false;
|
||||||
|
}
|
||||||
|
this.#updateTimeoutActive = !finished && this.#updateRate > 0;
|
||||||
|
if (this.#updateTimeoutActive) {
|
||||||
|
setTimeout(this.#startUpdate.bind(this), 1000 / this.#updateRate, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pointerMove(deltaX, deltaY) {
|
||||||
|
this.#moveXSum += deltaX;
|
||||||
|
this.#moveYSum += deltaY;
|
||||||
|
this.#startUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
pointerScroll(deltaHorizontal, deltaVertical, finish) {
|
||||||
|
this.#scrollHSum += deltaHorizontal;
|
||||||
|
this.#scrollVSum += deltaVertical;
|
||||||
|
this.#scrollFinish |= finish;
|
||||||
|
this.#startUpdate();
|
||||||
|
};
|
||||||
|
|
||||||
|
pointerButton(button, press) {
|
||||||
|
this.#socket.send("b" + button + ";" + (press ? 1 : 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
keyboardKey(key) {
|
||||||
|
this.#socket.send("k" + key);
|
||||||
|
}
|
||||||
|
|
||||||
|
keyboardText(text) {
|
||||||
|
this.#socket.send("t" + text);
|
||||||
|
}
|
||||||
|
}
|
||||||
74
webdata/app/keyboard.mjs
Normal file
74
webdata/app/keyboard.mjs
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2018-2019, 2023 Unrud <unrud@outlook.com>
|
||||||
|
*
|
||||||
|
* This file is part of Remote-Touchpad.
|
||||||
|
*
|
||||||
|
* Remote-Touchpad is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Remote-Touchpad is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Remote-Touchpad. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
KEY_SUPER, KEY_BACK_SPACE, KEY_RETURN, KEY_DELETE, KEY_HOME, KEY_END,
|
||||||
|
KEY_LEFT, KEY_RIGHT, KEY_UP, KEY_DOWN,
|
||||||
|
} from "./inputcontroller.mjs";
|
||||||
|
|
||||||
|
export default class Keyboard {
|
||||||
|
#inputController;
|
||||||
|
#checkAllowedCallback;
|
||||||
|
|
||||||
|
constructor(inputController, checkAllowedCallback) {
|
||||||
|
this.#inputController = inputController;
|
||||||
|
this.#checkAllowedCallback = checkAllowedCallback;
|
||||||
|
document.addEventListener("keydown", this.#handleKeydown.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
configure() {}
|
||||||
|
|
||||||
|
#handleKeydown(event) {
|
||||||
|
if (!this.#checkAllowedCallback() ||
|
||||||
|
event.ctrlKey || event.altKey || event.isComposing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let key = null;
|
||||||
|
if (event.key == "OS" || event.key == "Super" || event.key == "Meta") {
|
||||||
|
key = KEY_SUPER;
|
||||||
|
} else if (event.key == "Backspace") {
|
||||||
|
key = KEY_BACK_SPACE;
|
||||||
|
} else if (event.key == "Enter") {
|
||||||
|
key = KEY_RETURN;
|
||||||
|
} else if (event.key == "Delete") {
|
||||||
|
key = KEY_DELETE;
|
||||||
|
} else if (event.key == "Home") {
|
||||||
|
key = KEY_HOME;
|
||||||
|
} else if (event.key == "End") {
|
||||||
|
key = KEY_END;
|
||||||
|
} else if (event.key == "Left" || event.key == "ArrowLeft") {
|
||||||
|
key = KEY_LEFT;
|
||||||
|
} else if (event.key == "Right" || event.key == "ArrowRight") {
|
||||||
|
key = KEY_RIGHT;
|
||||||
|
} else if (event.key == "Up" || event.key == "ArrowUp") {
|
||||||
|
key = KEY_UP;
|
||||||
|
} else if (event.key == "Down" || event.key == "ArrowDown") {
|
||||||
|
key = KEY_DOWN;
|
||||||
|
}
|
||||||
|
if (key != null) {
|
||||||
|
if (!event.shiftKey) {
|
||||||
|
event.preventDefault();
|
||||||
|
this.#inputController.keyboardKey(key);
|
||||||
|
}
|
||||||
|
} else if (event.key.length == 1) {
|
||||||
|
event.preventDefault();
|
||||||
|
this.#inputController.keyboardText(event.key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
53
webdata/app/main.mjs
Normal file
53
webdata/app/main.mjs
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2018-2019, 2023 Unrud <unrud@outlook.com>
|
||||||
|
*
|
||||||
|
* This file is part of Remote-Touchpad.
|
||||||
|
*
|
||||||
|
* Remote-Touchpad is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Remote-Touchpad is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Remote-Touchpad. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import InputController, * as inputcontrollerModule from "./inputcontroller.mjs";
|
||||||
|
import Socket from "./socket.mjs";
|
||||||
|
import UI from "./ui.mjs";
|
||||||
|
|
||||||
|
const url = new URL("ws", location.href);
|
||||||
|
url.protocol = url.protocol == "http:" ? "ws:" : "wss:";
|
||||||
|
|
||||||
|
const socket = new Socket(url, window.location.hash.substr(1));
|
||||||
|
const inputController = new InputController(socket);
|
||||||
|
const ui = new UI(inputController);
|
||||||
|
|
||||||
|
socket.addEventListener("config", (event) => {
|
||||||
|
const config = event.detail;
|
||||||
|
inputController.configure(config);
|
||||||
|
ui.configure(config);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.addEventListener("close", () => {
|
||||||
|
ui.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
window.app = {
|
||||||
|
key: inputController.keyboardKey.bind(inputController),
|
||||||
|
text: inputController.keyboardText.bind(inputController),
|
||||||
|
toggleFullscreen: ui.toggleFullscreen.bind(ui),
|
||||||
|
showTextInput: ui.showTextInput.bind(ui),
|
||||||
|
showKeys: ui.showKeys.bind(ui),
|
||||||
|
setKeysPage: ui.setKeysPage.bind(ui),
|
||||||
|
};
|
||||||
|
for (const name in inputcontrollerModule) {
|
||||||
|
if (name.startsWith("KEY_")) {
|
||||||
|
window.app[name] = inputcontrollerModule[name];
|
||||||
|
}
|
||||||
|
}
|
||||||
74
webdata/app/mouse.mjs
Normal file
74
webdata/app/mouse.mjs
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2018-2019, 2023 Unrud <unrud@outlook.com>
|
||||||
|
*
|
||||||
|
* This file is part of Remote-Touchpad.
|
||||||
|
*
|
||||||
|
* Remote-Touchpad is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Remote-Touchpad is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Remote-Touchpad. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default class Mouse {
|
||||||
|
#moveSpeed = 1;
|
||||||
|
#scrollSpeed = 1;
|
||||||
|
|
||||||
|
#buttons = 0;
|
||||||
|
#inputController;
|
||||||
|
|
||||||
|
constructor (inputController, element) {
|
||||||
|
this.#inputController = inputController;
|
||||||
|
for (const type of ["touchstart", "touchend", "touchcancel", "touchmove"]) {
|
||||||
|
element.addEventListener(type, (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
element.addEventListener("mousedown", this.#handleMouseDownAndUp.bind(this));
|
||||||
|
element.addEventListener("mouseup", this.#handleMouseDownAndUp.bind(this));
|
||||||
|
element.addEventListener("mousemove", this.#handleMousemove.bind(this));
|
||||||
|
element.addEventListener("wheel", this.#handleWheel.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
configure(config) {
|
||||||
|
this.#moveSpeed = config.mouseMoveSpeed;
|
||||||
|
this.#scrollSpeed = config.mouseScrollSpeed;
|
||||||
|
}
|
||||||
|
|
||||||
|
#updateButtons(newButtons) {
|
||||||
|
for (let button = 0; button < 3; button += 1) {
|
||||||
|
const flag = 1 << button;
|
||||||
|
if ((newButtons&flag) != (this.#buttons & flag)) {
|
||||||
|
this.#inputController.pointerButton(button, newButtons & flag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.#buttons = newButtons;
|
||||||
|
}
|
||||||
|
|
||||||
|
#handleMouseDownAndUp(event) {
|
||||||
|
this.#updateButtons(event.buttons);
|
||||||
|
}
|
||||||
|
|
||||||
|
#handleMousemove(event) {
|
||||||
|
this.#inputController.pointerMove(
|
||||||
|
event.movementX * this.#moveSpeed, event.movementY * this.#moveSpeed);
|
||||||
|
}
|
||||||
|
|
||||||
|
#handleWheel(event) {
|
||||||
|
if (event.deltaMode == WheelEvent.DOM_DELTA_PIXEL) {
|
||||||
|
this.#inputController.pointerScroll(
|
||||||
|
event.deltaX * this.#scrollSpeed, event.deltaY * this.#scrollSpeed, true);
|
||||||
|
} else if (event.deltaMode == WheelEvent.DOM_DELTA_LINE) {
|
||||||
|
this.#inputController.pointerScroll(
|
||||||
|
event.deltaX * 20 * this.#scrollSpeed, event.deltaY * 20 * this.#scrollSpeed,
|
||||||
|
true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
9
webdata/app/sha256.mjs
Normal file
9
webdata/app/sha256.mjs
Normal file
File diff suppressed because one or more lines are too long
66
webdata/app/socket.mjs
Normal file
66
webdata/app/socket.mjs
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2018-2019, 2023 Unrud <unrud@outlook.com>
|
||||||
|
*
|
||||||
|
* This file is part of Remote-Touchpad.
|
||||||
|
*
|
||||||
|
* Remote-Touchpad is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Remote-Touchpad is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Remote-Touchpad. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import jsSHA from "./sha256.mjs"
|
||||||
|
|
||||||
|
const challengeResponse = (message, secret) => {
|
||||||
|
const shaObj = new jsSHA("SHA-256", "TEXT");
|
||||||
|
shaObj.setHMACKey(message, "TEXT");
|
||||||
|
shaObj.update(secret);
|
||||||
|
return btoa(shaObj.getHMAC("BYTES"));
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class Socket extends EventTarget {
|
||||||
|
#secret;
|
||||||
|
#authenticated;
|
||||||
|
#ws;
|
||||||
|
|
||||||
|
constructor(url, secret) {
|
||||||
|
super();
|
||||||
|
this.#secret = secret;
|
||||||
|
this.#authenticated = false;
|
||||||
|
this.#ws = new WebSocket(url);
|
||||||
|
this.#ws.addEventListener("message", this.#handle_ws_message.bind(this));
|
||||||
|
this.#ws.addEventListener("close", this.#handle_ws_close.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
#handle_ws_message(event) {
|
||||||
|
if (!this.#authenticated) {
|
||||||
|
this.#ws.send(challengeResponse(event.data, this.#secret));
|
||||||
|
this.#authenticated = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let config;
|
||||||
|
try {
|
||||||
|
config = JSON.parse(event.data);
|
||||||
|
} catch (e) {
|
||||||
|
this.#ws.close();
|
||||||
|
throw (e);
|
||||||
|
}
|
||||||
|
this.dispatchEvent(new CustomEvent("config", {detail: config}));
|
||||||
|
}
|
||||||
|
|
||||||
|
#handle_ws_close() {
|
||||||
|
this.dispatchEvent(new CustomEvent("close"));
|
||||||
|
}
|
||||||
|
|
||||||
|
send(message) {
|
||||||
|
this.#ws.send(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
232
webdata/app/touchpad.mjs
Normal file
232
webdata/app/touchpad.mjs
Normal file
|
|
@ -0,0 +1,232 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2018-2019, 2023 Unrud <unrud@outlook.com>
|
||||||
|
*
|
||||||
|
* This file is part of Remote-Touchpad.
|
||||||
|
*
|
||||||
|
* Remote-Touchpad is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Remote-Touchpad is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Remote-Touchpad. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {POINTER_BUTTON_LEFT, POINTER_BUTTON_MIDDLE, POINTER_BUTTON_RIGHT} from "./inputcontroller.mjs";
|
||||||
|
|
||||||
|
// [1 Touch, 2 Touches, 3 Touches] (as pixel)
|
||||||
|
const TOUCH_MOVE_THRESHOLD = [10, 15, 15];
|
||||||
|
// Max time between consecutive touches for clicking or dragging (as milliseconds)
|
||||||
|
const TOUCH_TIMEOUT = 250;
|
||||||
|
// [[pixel/second, multiplicator], ...]
|
||||||
|
const POINTER_ACCELERATION = [
|
||||||
|
[0, 0],
|
||||||
|
[87, 1],
|
||||||
|
[173, 1],
|
||||||
|
[553, 2],
|
||||||
|
];
|
||||||
|
|
||||||
|
const copyTouch = (touch, timeStamp) => ({
|
||||||
|
identifier: touch.identifier,
|
||||||
|
pageX: touch.pageX,
|
||||||
|
pageXStart: touch.pageX,
|
||||||
|
pageY: touch.pageY,
|
||||||
|
pageYStart: touch.pageY,
|
||||||
|
timeStamp: timeStamp,
|
||||||
|
});
|
||||||
|
|
||||||
|
const calculateAccelerationMult = (speed) => {
|
||||||
|
for (let i = 0; i < POINTER_ACCELERATION.length; i += 1) {
|
||||||
|
const s2 = POINTER_ACCELERATION[i][0];
|
||||||
|
const a2 = POINTER_ACCELERATION[i][1];
|
||||||
|
if (s2 <= speed) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (i == 0) {
|
||||||
|
return a2;
|
||||||
|
}
|
||||||
|
const s1 = POINTER_ACCELERATION[i - 1][0];
|
||||||
|
const a1 = POINTER_ACCELERATION[i - 1][1];
|
||||||
|
return ((speed - s1) / (s2 - s1)) * (a2 - a1) + a1;
|
||||||
|
}
|
||||||
|
if (POINTER_ACCELERATION.length > 0) {
|
||||||
|
return POINTER_ACCELERATION[POINTER_ACCELERATION.length - 1][1];
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class Touchpad {
|
||||||
|
#moveSpeed = 1;
|
||||||
|
#scrollSpeed = 1;
|
||||||
|
|
||||||
|
#moved = false;
|
||||||
|
#startTimeStamp = 0;
|
||||||
|
#lastEndTimeStamp = 0;
|
||||||
|
#releasedCount = 0;
|
||||||
|
#ongoingTouches = [];
|
||||||
|
#dragging = false;
|
||||||
|
#draggingTimeout = null;
|
||||||
|
#inputController;
|
||||||
|
#checkAllowedCallback;
|
||||||
|
|
||||||
|
constructor(inputController, checkAllowedCallback) {
|
||||||
|
this.#inputController = inputController;
|
||||||
|
this.#checkAllowedCallback = checkAllowedCallback;
|
||||||
|
document.addEventListener("touchstart", this.#handleTouchstart.bind(this));
|
||||||
|
document.addEventListener("touchend", this.#handleTouchend.bind(this));
|
||||||
|
document.addEventListener("touchcancel", this.#handleTouchend.bind(this));
|
||||||
|
document.addEventListener("touchmove", this.#handleTouchmove.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
configure(config) {
|
||||||
|
this.#moveSpeed = config.moveSpeed;
|
||||||
|
this.#scrollSpeed = config.scrollSpeed;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ongoingTouchIndexById(idToFind) {
|
||||||
|
for (let i = 0; i < this.#ongoingTouches.length; i += 1) {
|
||||||
|
if (this.#ongoingTouches[i].identifier == idToFind) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#handleDraggingTimeout() {
|
||||||
|
this.#draggingTimeout = null;
|
||||||
|
this.#inputController.pointerButton(POINTER_BUTTON_LEFT, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
#handleTouchstart(event) {
|
||||||
|
// Might get called multiple times for the same touches
|
||||||
|
if (this.#ongoingTouches.length == 0) {
|
||||||
|
this.#startTimeStamp = event.timeStamp;
|
||||||
|
this.#moved = false;
|
||||||
|
}
|
||||||
|
const touches = event.changedTouches;
|
||||||
|
let foundTouch = false;
|
||||||
|
for (let i = 0; i < touches.length; i += 1) {
|
||||||
|
if (this.#ongoingTouches.length == 0 &&
|
||||||
|
!this.#checkAllowedCallback(touches[i].target)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
foundTouch = true;
|
||||||
|
const touch = copyTouch(touches[i], event.timeStamp);
|
||||||
|
const idx = this.#ongoingTouchIndexById(touch.identifier);
|
||||||
|
if (idx < 0) {
|
||||||
|
this.#ongoingTouches.push(touch);
|
||||||
|
} else {
|
||||||
|
this.#ongoingTouches[idx] = touch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!foundTouch) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
|
this.#lastEndTimeStamp = 0;
|
||||||
|
if (this.#draggingTimeout != null) {
|
||||||
|
clearTimeout(this.#draggingTimeout);
|
||||||
|
this.#draggingTimeout = null;
|
||||||
|
this.#dragging = true;
|
||||||
|
}
|
||||||
|
this.#inputController.pointerScroll(0, 0, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
#handleTouchend(event) {
|
||||||
|
const touches = event.changedTouches;
|
||||||
|
let foundTouch = false;
|
||||||
|
for (let i = 0; i < touches.length; i += 1) {
|
||||||
|
const idx = this.#ongoingTouchIndexById(touches[i].identifier);
|
||||||
|
if (idx < 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
foundTouch = true;
|
||||||
|
this.#ongoingTouches.splice(idx, 1);
|
||||||
|
this.#releasedCount += 1;
|
||||||
|
}
|
||||||
|
if (!foundTouch) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
|
this.#lastEndTimeStamp = event.timeStamp;
|
||||||
|
this.#inputController.pointerScroll(0, 0, true);
|
||||||
|
if (this.#releasedCount > TOUCH_MOVE_THRESHOLD.length) {
|
||||||
|
this.#moved = true;
|
||||||
|
}
|
||||||
|
if (this.#ongoingTouches.length == 0 && this.#releasedCount >= 1) {
|
||||||
|
if (this.#dragging) {
|
||||||
|
this.#dragging = false;
|
||||||
|
this.#inputController.pointerButton(POINTER_BUTTON_LEFT, false);
|
||||||
|
}
|
||||||
|
if (!this.#moved && event.timeStamp - this.#startTimeStamp < TOUCH_TIMEOUT) {
|
||||||
|
let button = 0;
|
||||||
|
if (this.#releasedCount == 1) {
|
||||||
|
button = POINTER_BUTTON_LEFT;
|
||||||
|
} else if (this.#releasedCount == 2) {
|
||||||
|
button = POINTER_BUTTON_RIGHT;
|
||||||
|
} else if (this.#releasedCount == 3) {
|
||||||
|
button = POINTER_BUTTON_MIDDLE;
|
||||||
|
}
|
||||||
|
this.#inputController.pointerButton(button, true);
|
||||||
|
if (button == POINTER_BUTTON_LEFT) {
|
||||||
|
this.#draggingTimeout = setTimeout(
|
||||||
|
this.#handleDraggingTimeout.bind(this), TOUCH_TIMEOUT);
|
||||||
|
} else {
|
||||||
|
this.#inputController.pointerButton(button, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.#releasedCount = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#handleTouchmove(event) {
|
||||||
|
let sumX = 0;
|
||||||
|
let sumY = 0;
|
||||||
|
const touches = event.changedTouches;
|
||||||
|
let foundTouch = false;
|
||||||
|
for (let i = 0; i < touches.length; i += 1) {
|
||||||
|
const idx = this.#ongoingTouchIndexById(touches[i].identifier);
|
||||||
|
if (idx < 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
foundTouch = true;
|
||||||
|
if (!this.#moved) {
|
||||||
|
const dist = Math.sqrt(
|
||||||
|
Math.pow(touches[i].pageX - this.#ongoingTouches[idx].pageXStart, 2) +
|
||||||
|
Math.pow(touches[i].pageY - this.#ongoingTouches[idx].pageYStart, 2)
|
||||||
|
);
|
||||||
|
if (this.#ongoingTouches.length > TOUCH_MOVE_THRESHOLD.length ||
|
||||||
|
dist > TOUCH_MOVE_THRESHOLD[this.#ongoingTouches.length - 1] ||
|
||||||
|
event.timeStamp - this.#startTimeStamp >= TOUCH_TIMEOUT) {
|
||||||
|
this.#moved = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const dx = touches[i].pageX - this.#ongoingTouches[idx].pageX;
|
||||||
|
const dy = touches[i].pageY - this.#ongoingTouches[idx].pageY;
|
||||||
|
const timeDelta = event.timeStamp - this.#ongoingTouches[idx].timeStamp;
|
||||||
|
sumX += dx * calculateAccelerationMult(Math.abs(dx) / timeDelta * 1000);
|
||||||
|
sumY += dy * calculateAccelerationMult(Math.abs(dy) / timeDelta * 1000);
|
||||||
|
this.#ongoingTouches[idx].pageX = touches[i].pageX;
|
||||||
|
this.#ongoingTouches[idx].pageY = touches[i].pageY;
|
||||||
|
this.#ongoingTouches[idx].timeStamp = event.timeStamp;
|
||||||
|
}
|
||||||
|
if (!foundTouch) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
|
if (this.#moved && event.timeStamp - this.#lastEndTimeStamp >= TOUCH_TIMEOUT) {
|
||||||
|
if (this.#ongoingTouches.length == 1 || this.#dragging) {
|
||||||
|
this.#inputController.pointerMove(
|
||||||
|
sumX * this.#moveSpeed, sumY * this.#moveSpeed);
|
||||||
|
} else if (this.#ongoingTouches.length == 2) {
|
||||||
|
this.#inputController.pointerScroll(
|
||||||
|
-sumX * this.#scrollSpeed, -sumY * this.#scrollSpeed, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
199
webdata/app/ui.mjs
Normal file
199
webdata/app/ui.mjs
Normal file
|
|
@ -0,0 +1,199 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2018-2019, 2023 Unrud <unrud@outlook.com>
|
||||||
|
*
|
||||||
|
* This file is part of Remote-Touchpad.
|
||||||
|
*
|
||||||
|
* Remote-Touchpad is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Remote-Touchpad is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Remote-Touchpad. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Keyboard from "./keyboard.mjs";
|
||||||
|
import Mouse from "./mouse.mjs";
|
||||||
|
import Touchpad from "./touchpad.mjs";
|
||||||
|
import * as compat from "./compat.mjs";
|
||||||
|
|
||||||
|
const IGNORE_CLICK_AFTER_TOUCH_DURATION = 1000; // milliseconds
|
||||||
|
const CLICK_VIBRATION_PATTERN = [10];
|
||||||
|
|
||||||
|
const buttons = document.querySelectorAll("button");
|
||||||
|
const scenes = document.querySelectorAll("body > .scene");
|
||||||
|
const openingScene = document.getElementById("opening");
|
||||||
|
const closedScene = document.getElementById("closed");
|
||||||
|
const padScene = document.getElementById("pad");
|
||||||
|
const keysScene = document.getElementById("keys");
|
||||||
|
const keysPages = keysScene.querySelectorAll(":scope > .page");
|
||||||
|
const textInputScene = document.getElementById("text-input");
|
||||||
|
const textInput = textInputScene.querySelector("textarea");
|
||||||
|
const mouseScene = document.getElementById("mouse");
|
||||||
|
const sendText = document.getElementById("send-text");
|
||||||
|
|
||||||
|
export default class UI {
|
||||||
|
#activeScene = null;
|
||||||
|
#keysActiveName = "";
|
||||||
|
#ready = false;
|
||||||
|
#closed = false;
|
||||||
|
#ignoreClickUntilTimeStamp = Number.MIN_VALUE;
|
||||||
|
#inputController;
|
||||||
|
#mouse;
|
||||||
|
#keyboard;
|
||||||
|
#touchpad;
|
||||||
|
|
||||||
|
constructor(inputController) {
|
||||||
|
this.#inputController = inputController;
|
||||||
|
this.#mouse = new Mouse(inputController, mouseScene);
|
||||||
|
this.#keyboard = new Keyboard(inputController,
|
||||||
|
() => this.#activeScene?.classList.contains("keyboard-input"));
|
||||||
|
this.#touchpad = new Touchpad(inputController,
|
||||||
|
(target) => target.classList.contains("touch-input"));
|
||||||
|
document.addEventListener("mousedown", this.#handleMousedown.bind(this));
|
||||||
|
document.addEventListener("touchend", this.#handleTouchend.bind(this));
|
||||||
|
textInput.addEventListener("input", () => { this.#updateTextInput(); });
|
||||||
|
sendText.addEventListener("click", this.#handleSendText.bind(this));
|
||||||
|
window.addEventListener("popstate", () => { this.#update(); });
|
||||||
|
compat.addFullscreenchangeEventListener(() => { this.#update(); });
|
||||||
|
compat.addPointerlockchangeEventListener(() => { this.#update(); });
|
||||||
|
for (const button of buttons) {
|
||||||
|
button.setAttribute("tabindex", "-1");
|
||||||
|
button.addEventListener("click", this.#handleButtonClick.bind(this));
|
||||||
|
}
|
||||||
|
this.#update();
|
||||||
|
}
|
||||||
|
|
||||||
|
configure(config) {
|
||||||
|
this.#mouse.configure(config);
|
||||||
|
this.#keyboard.configure(config);
|
||||||
|
this.#touchpad.configure(config);
|
||||||
|
if (!this.#closed) {
|
||||||
|
this.#ready = true;
|
||||||
|
}
|
||||||
|
this.#update();
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.#ready = false;
|
||||||
|
this.#closed = true;
|
||||||
|
this.#update();
|
||||||
|
}
|
||||||
|
|
||||||
|
#handleTouchend(event) {
|
||||||
|
// HACK: event.preventDefault doesn't reliably stop click events in Firefox (112)
|
||||||
|
this.#ignoreClickUntilTimeStamp = event.timeStamp + IGNORE_CLICK_AFTER_TOUCH_DURATION;
|
||||||
|
}
|
||||||
|
|
||||||
|
#handleMousedown(event) {
|
||||||
|
if (this.#activeScene != mouseScene && event.buttons == 1 &&
|
||||||
|
this.#ignoreClickUntilTimeStamp <= event.timeStamp &&
|
||||||
|
event.target.classList.contains("mouse-input")) {
|
||||||
|
compat.requestPointerLock(mouseScene);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#updateTextInput(newValue = null) {
|
||||||
|
if (newValue != null) {
|
||||||
|
textInput.value = newValue;
|
||||||
|
}
|
||||||
|
sessionStorage.setItem("text-input", textInput.value);
|
||||||
|
sendText.textContent = sendText.getAttribute(`data-${textInput.value ? "send" : "back"}-content`);
|
||||||
|
textInput.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
#handleButtonClick(event) {
|
||||||
|
event.target.classList.add("click");
|
||||||
|
setTimeout(() => event.target.classList.remove("click"), 0);
|
||||||
|
compat.vibrate(CLICK_VIBRATION_PATTERN);
|
||||||
|
}
|
||||||
|
|
||||||
|
#handleSendText() {
|
||||||
|
if (textInput.value) {
|
||||||
|
// normalize line endings
|
||||||
|
const text = textInput.value.replace(/\r\n?/g, "\n");
|
||||||
|
this.#inputController.keyboardText(text);
|
||||||
|
this.#updateTextInput("");
|
||||||
|
} else {
|
||||||
|
history.back();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#showScene(scene) {
|
||||||
|
this.#activeScene = scene;
|
||||||
|
if (compat.fullscreenElement() && !scene.classList.contains("allow-fullscreen")) {
|
||||||
|
compat.exitFullscreen();
|
||||||
|
}
|
||||||
|
if (compat.pointerLockElement() && this.#activeScene != mouseScene) {
|
||||||
|
compat.exitPointerLock();
|
||||||
|
}
|
||||||
|
textInput.value = "";
|
||||||
|
for (const otherScene of scenes) {
|
||||||
|
otherScene.classList.toggle("hidden", otherScene != scene);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setKeysPage(index, relative = false) {
|
||||||
|
if (relative) {
|
||||||
|
for (let i = 0; i < keysPages.length && keysPages[i].classList.contains("hidden");
|
||||||
|
i += 1, index += 1);
|
||||||
|
}
|
||||||
|
index = ((index % keysPages.length) + keysPages.length) % keysPages.length;
|
||||||
|
sessionStorage.setItem(this.#keysActiveName, index);
|
||||||
|
for (let i = 0; i < keysPages.length; i += 1) {
|
||||||
|
keysPages[i].classList.toggle("hidden", i != index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showKeys(name = "", defaultPageIndex = 0) {
|
||||||
|
this.#showScene(keysScene);
|
||||||
|
this.#keysActiveName = "keys" + (name ? ":" + name : "");
|
||||||
|
let pageIndex = parseInt(sessionStorage.getItem(this.#keysActiveName));
|
||||||
|
if (isNaN(pageIndex)) {
|
||||||
|
pageIndex = defaultPageIndex;
|
||||||
|
}
|
||||||
|
this.setKeysPage(pageIndex);
|
||||||
|
if (history.state != this.#keysActiveName) {
|
||||||
|
history.pushState(this.#keysActiveName, "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showTextInput() {
|
||||||
|
this.#showScene(textInputScene);
|
||||||
|
this.#updateTextInput(sessionStorage.getItem("text-input") || "");
|
||||||
|
if (history.state != "text-input") {
|
||||||
|
history.pushState("text-input", "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleFullscreen() {
|
||||||
|
if (compat.fullscreenElement()) {
|
||||||
|
compat.exitFullscreen();
|
||||||
|
} else {
|
||||||
|
compat.requestFullscreen(document.documentElement, {navigationUI: "hide"});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#update() {
|
||||||
|
const fullscreenEnabled = compat.fullscreenEnabled();
|
||||||
|
for (const element of document.querySelectorAll(".visble-if-fullscreen-enabled")) {
|
||||||
|
element.classList.toggle("hidden", !fullscreenEnabled);
|
||||||
|
}
|
||||||
|
if (!this.#ready) {
|
||||||
|
this.#showScene(this.#closed ? closedScene : openingScene);
|
||||||
|
} else if (compat.pointerLockElement()) {
|
||||||
|
this.#showScene(mouseScene);
|
||||||
|
} else if ((history.state || "").split(":")[0] == "keys") {
|
||||||
|
this.showKeys(history.state.substr("keys:".length));
|
||||||
|
} else if (history.state == "text-input") {
|
||||||
|
this.showTextInput();
|
||||||
|
} else {
|
||||||
|
this.#showScene(padScene);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
696
webdata/fn.js
696
webdata/fn.js
|
|
@ -1,696 +0,0 @@
|
||||||
"use strict";
|
|
||||||
/*
|
|
||||||
* Copyright (c) 2018-2019 Unrud <unrud@outlook.com>
|
|
||||||
*
|
|
||||||
* This file is part of Remote-Touchpad.
|
|
||||||
*
|
|
||||||
* Remote-Touchpad is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Remote-Touchpad is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with Remote-Touchpad. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// [1 Touch, 2 Touches, 3 Touches] (as pixel)
|
|
||||||
var TOUCH_MOVE_THRESHOLD = [10, 15, 15];
|
|
||||||
// Max time between consecutive touches for clicking or dragging (as milliseconds)
|
|
||||||
var TOUCH_TIMEOUT = 250;
|
|
||||||
// [[pixel/second, multiplicator], ...]
|
|
||||||
var POINTER_ACCELERATION = [
|
|
||||||
[0, 0],
|
|
||||||
[87, 1],
|
|
||||||
[173, 1],
|
|
||||||
[553, 2]
|
|
||||||
];
|
|
||||||
|
|
||||||
var POINTER_BUTTON_LEFT = 0;
|
|
||||||
var POINTER_BUTTON_RIGHT = 1;
|
|
||||||
var POINTER_BUTTON_MIDDLE = 2;
|
|
||||||
|
|
||||||
var KEY_VOLUME_MUTE = 0;
|
|
||||||
var KEY_VOLUME_DOWN = 1;
|
|
||||||
var KEY_VOLUME_UP = 2;
|
|
||||||
var KEY_MEDIA_PLAY_PAUSE = 3;
|
|
||||||
var KEY_MEDIA_PREV_TRACK = 4;
|
|
||||||
var KEY_MEDIA_NEXT_TRACK = 5;
|
|
||||||
var KEY_BROWSER_BACK = 6;
|
|
||||||
var KEY_BROWSER_FORWARD = 7;
|
|
||||||
var KEY_SUPER = 8;
|
|
||||||
var KEY_LEFT = 9;
|
|
||||||
var KEY_RIGHT = 10;
|
|
||||||
var KEY_UP = 11;
|
|
||||||
var KEY_DOWN = 12;
|
|
||||||
var KEY_HOME = 13;
|
|
||||||
var KEY_END = 14;
|
|
||||||
var KEY_BACK_SPACE = 15;
|
|
||||||
var KEY_DELETE = 16;
|
|
||||||
var KEY_RETURN = 17;
|
|
||||||
|
|
||||||
var ws = null;
|
|
||||||
var config = null;
|
|
||||||
|
|
||||||
var util = (function() {
|
|
||||||
var util = {};
|
|
||||||
|
|
||||||
util.fullscreenEnabled = function() {
|
|
||||||
return (document.fullscreenEnabled ||
|
|
||||||
document.webkitFullscreenEnabled ||
|
|
||||||
document.mozFullScreenEnabled ||
|
|
||||||
document.msFullscreenEnabled ||
|
|
||||||
false);
|
|
||||||
};
|
|
||||||
|
|
||||||
util.requestFullscreen = function(element, options) {
|
|
||||||
if (element.requestFullscreen) {
|
|
||||||
element.requestFullscreen(options);
|
|
||||||
} else if (element.webkitRequestFullscreen) {
|
|
||||||
element.webkitRequestFullscreen(options);
|
|
||||||
} else if (element.mozRequestFullScreen) {
|
|
||||||
element.mozRequestFullScreen(options);
|
|
||||||
} else if (element.msRequestFullscreen) {
|
|
||||||
element.msRequestFullscreen(options);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
util.exitFullscreen = function() {
|
|
||||||
if (document.exitFullscreen) {
|
|
||||||
document.exitFullscreen();
|
|
||||||
} else if (document.webkitExitFullscreen) {
|
|
||||||
document.webkitExitFullscreen();
|
|
||||||
} else if (document.mozCancelFullScreen) {
|
|
||||||
document.mozCancelFullScreen();
|
|
||||||
} else if (document.msExitFullscreen) {
|
|
||||||
document.msExitFullscreen();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
util.fullscreenElement = function() {
|
|
||||||
return (document.fullscreenElement ||
|
|
||||||
document.webkitFullscreenElement ||
|
|
||||||
document.mozFullScreenElement ||
|
|
||||||
document.msFullscreenElement ||
|
|
||||||
null);
|
|
||||||
};
|
|
||||||
|
|
||||||
util.addFullscreenchangeEventListener = function(listener) {
|
|
||||||
if ("onfullscreenchange" in document) {
|
|
||||||
document.addEventListener("fullscreenchange", listener);
|
|
||||||
} else if ("onwebkitfullscreenchange" in document) {
|
|
||||||
document.addEventListener("webkitfullscreenchange", listener);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
util.requestPointerLock = function(element) {
|
|
||||||
if (element.requestPointerLock) {
|
|
||||||
element.requestPointerLock();
|
|
||||||
} else if (element.mozRequestPointerLock) {
|
|
||||||
element.mozRequestPointerLock();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
util.exitPointerLock = function() {
|
|
||||||
if (document.exitPointerLock) {
|
|
||||||
document.exitPointerLock();
|
|
||||||
} else if (document.mozExitPointerLock) {
|
|
||||||
document.mozExitPointerLock();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
util.pointerLockElement = function() {
|
|
||||||
return (document.pointerLockElement ||
|
|
||||||
document.mozPointerLockElement ||
|
|
||||||
null);
|
|
||||||
};
|
|
||||||
|
|
||||||
util.addPointerlockchangeEventListener = function(listener) {
|
|
||||||
if ("onpointerlockchange" in document) {
|
|
||||||
document.addEventListener("pointerlockchange", listener);
|
|
||||||
} else if ("onmozpointerlockchange" in document) {
|
|
||||||
document.addEventListener("mozpointerlockchange", listener);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return util;
|
|
||||||
})();
|
|
||||||
|
|
||||||
var controller = (function() {
|
|
||||||
var controller = {};
|
|
||||||
|
|
||||||
var moveXSum = 0;
|
|
||||||
var moveYSum = 0;
|
|
||||||
var scrollHSum = 0;
|
|
||||||
var scrollVSum = 0;
|
|
||||||
var scrolling = false;
|
|
||||||
var scrollFinish = false;
|
|
||||||
var updateTimoueActive = false;
|
|
||||||
|
|
||||||
function startUpdate(fromTimeout) {
|
|
||||||
if (updateTimoueActive && !fromTimeout) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
updateTimoueActive = false;
|
|
||||||
var finished = true;
|
|
||||||
var xInt = Math.trunc(moveXSum);
|
|
||||||
var yInt = Math.trunc(moveYSum);
|
|
||||||
if (xInt != 0 || yInt != 0) {
|
|
||||||
ws.send("m" + xInt + ";" + yInt);
|
|
||||||
moveXSum -= xInt;
|
|
||||||
moveYSum -= yInt;
|
|
||||||
finished = false;
|
|
||||||
}
|
|
||||||
var hInt = Math.trunc(scrollHSum);
|
|
||||||
var vInt = Math.trunc(scrollVSum);
|
|
||||||
if (hInt != 0 || vInt != 0) {
|
|
||||||
ws.send((scrollFinish ? "S" : "s") + hInt + ";" + vInt);
|
|
||||||
scrollHSum -= hInt;
|
|
||||||
scrollVSum -= vInt;
|
|
||||||
scrolling = !scrollFinish;
|
|
||||||
scrollFinish = false;
|
|
||||||
finished = false;
|
|
||||||
} else if (scrollFinish && scrolling) {
|
|
||||||
ws.send("S");
|
|
||||||
scrolling = false;
|
|
||||||
scrollFinish = false;
|
|
||||||
}
|
|
||||||
updateTimoueActive = !finished && config.updateRate > 0;
|
|
||||||
if (updateTimoueActive) {
|
|
||||||
setTimeout(startUpdate, 1000/config.updateRate, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
controller.pointerMove = function(deltaX, deltaY) {
|
|
||||||
moveXSum += deltaX;
|
|
||||||
moveYSum += deltaY;
|
|
||||||
startUpdate();
|
|
||||||
};
|
|
||||||
|
|
||||||
controller.pointerScroll = function(deltaHorizontal, deltaVertical, finish) {
|
|
||||||
scrollHSum += deltaHorizontal;
|
|
||||||
scrollVSum += deltaVertical;
|
|
||||||
scrollFinish |= finish;
|
|
||||||
startUpdate();
|
|
||||||
};
|
|
||||||
|
|
||||||
controller.pointerButton = function(button, press) {
|
|
||||||
ws.send("b" + button + ";" + (press ? 1 : 0));
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
controller.keyboardKey = function(key) {
|
|
||||||
ws.send("k" + key);
|
|
||||||
};
|
|
||||||
|
|
||||||
controller.keyboardText = function(text) {
|
|
||||||
ws.send("t" + text);
|
|
||||||
};
|
|
||||||
|
|
||||||
return controller;
|
|
||||||
})();
|
|
||||||
|
|
||||||
var touchpad = (function() {
|
|
||||||
var touchpad = {};
|
|
||||||
|
|
||||||
var moved = false;
|
|
||||||
var startTimeStamp = 0;
|
|
||||||
var lastEndTimeStamp = 0;
|
|
||||||
var releasedCount = 0;
|
|
||||||
var ongoingTouches = [];
|
|
||||||
var dragging = false;
|
|
||||||
var draggingTimeout = null;
|
|
||||||
|
|
||||||
function copyTouch(touch, timeStamp) {
|
|
||||||
return {
|
|
||||||
identifier: touch.identifier,
|
|
||||||
pageX: touch.pageX,
|
|
||||||
pageXStart: touch.pageX,
|
|
||||||
pageY: touch.pageY,
|
|
||||||
pageYStart: touch.pageY,
|
|
||||||
timeStamp: timeStamp
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function ongoingTouchIndexById(idToFind) {
|
|
||||||
for (var i = 0; i < ongoingTouches.length; i += 1) {
|
|
||||||
if (ongoingTouches[i].identifier == idToFind) {
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
function calculateAccelerationMult(speed) {
|
|
||||||
for (var i = 0; i < POINTER_ACCELERATION.length; i += 1) {
|
|
||||||
var s2 = POINTER_ACCELERATION[i][0];
|
|
||||||
var a2 = POINTER_ACCELERATION[i][1];
|
|
||||||
if (s2 <= speed) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (i == 0) {
|
|
||||||
return a2;
|
|
||||||
}
|
|
||||||
var s1 = POINTER_ACCELERATION[i - 1][0];
|
|
||||||
var a1 = POINTER_ACCELERATION[i - 1][1];
|
|
||||||
return ((speed - s1) / (s2 - s1)) * (a2 - a1) + a1;
|
|
||||||
}
|
|
||||||
if (POINTER_ACCELERATION.length > 0) {
|
|
||||||
return POINTER_ACCELERATION[POINTER_ACCELERATION.length - 1][1];
|
|
||||||
}
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
function onDraggingTimeout() {
|
|
||||||
draggingTimeout = null;
|
|
||||||
controller.pointerButton(POINTER_BUTTON_LEFT, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
touchpad.handleTouchstart = function(evt) {
|
|
||||||
// Might get called multiple times for the same touches
|
|
||||||
if (ongoingTouches.length == 0) {
|
|
||||||
startTimeStamp = evt.timeStamp;
|
|
||||||
moved = false;
|
|
||||||
}
|
|
||||||
var touches = evt.changedTouches;
|
|
||||||
var foundTouch = false;
|
|
||||||
for (var i = 0; i < touches.length; i += 1) {
|
|
||||||
if (ongoingTouches.length == 0 && !touches[i].target.classList.contains("touch")) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
foundTouch = true;
|
|
||||||
var touch = copyTouch(touches[i], evt.timeStamp);
|
|
||||||
var idx = ongoingTouchIndexById(touch.identifier);
|
|
||||||
if (idx < 0) {
|
|
||||||
ongoingTouches.push(touch);
|
|
||||||
} else {
|
|
||||||
ongoingTouches[idx] = touch;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!foundTouch) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
evt.preventDefault();
|
|
||||||
lastEndTimeStamp = 0;
|
|
||||||
if (draggingTimeout != null) {
|
|
||||||
clearTimeout(draggingTimeout);
|
|
||||||
draggingTimeout = null;
|
|
||||||
dragging = true;
|
|
||||||
}
|
|
||||||
controller.pointerScroll(0, 0, true);
|
|
||||||
};
|
|
||||||
|
|
||||||
touchpad.handleTouchend = touchpad.handleTouchcancel = function(evt) {
|
|
||||||
var touches = evt.changedTouches;
|
|
||||||
var foundTouch = false;
|
|
||||||
for (var i = 0; i < touches.length; i += 1) {
|
|
||||||
var idx = ongoingTouchIndexById(touches[i].identifier);
|
|
||||||
if (idx < 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
foundTouch = true;
|
|
||||||
ongoingTouches.splice(idx, 1);
|
|
||||||
releasedCount += 1;
|
|
||||||
}
|
|
||||||
if (!foundTouch) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
evt.preventDefault();
|
|
||||||
lastEndTimeStamp = evt.timeStamp;
|
|
||||||
controller.pointerScroll(0, 0, true);
|
|
||||||
if (releasedCount > TOUCH_MOVE_THRESHOLD.length) {
|
|
||||||
moved = true;
|
|
||||||
}
|
|
||||||
if (ongoingTouches.length == 0 && releasedCount >= 1) {
|
|
||||||
if (dragging) {
|
|
||||||
dragging = false;
|
|
||||||
controller.pointerButton(POINTER_BUTTON_LEFT, false);
|
|
||||||
}
|
|
||||||
if (!moved && evt.timeStamp - startTimeStamp < TOUCH_TIMEOUT) {
|
|
||||||
var button = 0;
|
|
||||||
if (releasedCount == 1) {
|
|
||||||
button = POINTER_BUTTON_LEFT;
|
|
||||||
} else if (releasedCount == 2) {
|
|
||||||
button = POINTER_BUTTON_RIGHT;
|
|
||||||
} else if (releasedCount == 3) {
|
|
||||||
button = POINTER_BUTTON_MIDDLE;
|
|
||||||
}
|
|
||||||
controller.pointerButton(button, true);
|
|
||||||
if (button == POINTER_BUTTON_LEFT) {
|
|
||||||
draggingTimeout = setTimeout(onDraggingTimeout, TOUCH_TIMEOUT);
|
|
||||||
} else {
|
|
||||||
controller.pointerButton(button, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
releasedCount = 0;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
touchpad.handleTouchmove = function(evt) {
|
|
||||||
var sumX = 0;
|
|
||||||
var sumY = 0;
|
|
||||||
var touches = evt.changedTouches;
|
|
||||||
var foundTouch = false;
|
|
||||||
for (var i = 0; i < touches.length; i += 1) {
|
|
||||||
var idx = ongoingTouchIndexById(touches[i].identifier);
|
|
||||||
if (idx < 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
foundTouch = true;
|
|
||||||
if (!moved) {
|
|
||||||
var dist = Math.sqrt(Math.pow(touches[i].pageX - ongoingTouches[idx].pageXStart, 2) +
|
|
||||||
Math.pow(touches[i].pageY - ongoingTouches[idx].pageYStart, 2));
|
|
||||||
if (ongoingTouches.length > TOUCH_MOVE_THRESHOLD.length ||
|
|
||||||
dist > TOUCH_MOVE_THRESHOLD[ongoingTouches.length - 1] ||
|
|
||||||
evt.timeStamp - startTimeStamp >= TOUCH_TIMEOUT) {
|
|
||||||
moved = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var dx = touches[i].pageX - ongoingTouches[idx].pageX;
|
|
||||||
var dy = touches[i].pageY - ongoingTouches[idx].pageY;
|
|
||||||
var timeDelta = evt.timeStamp - ongoingTouches[idx].timeStamp;
|
|
||||||
sumX += dx * calculateAccelerationMult(Math.abs(dx) / timeDelta * 1000);
|
|
||||||
sumY += dy * calculateAccelerationMult(Math.abs(dy) / timeDelta * 1000);
|
|
||||||
ongoingTouches[idx].pageX = touches[i].pageX;
|
|
||||||
ongoingTouches[idx].pageY = touches[i].pageY;
|
|
||||||
ongoingTouches[idx].timeStamp = evt.timeStamp;
|
|
||||||
}
|
|
||||||
if (!foundTouch) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
evt.preventDefault();
|
|
||||||
if (moved && evt.timeStamp - lastEndTimeStamp >= TOUCH_TIMEOUT) {
|
|
||||||
if (ongoingTouches.length == 1 || dragging) {
|
|
||||||
controller.pointerMove(sumX*config.moveSpeed, sumY*config.moveSpeed);
|
|
||||||
} else if (ongoingTouches.length == 2) {
|
|
||||||
controller.pointerScroll(-sumX*config.scrollSpeed, -sumY*config.scrollSpeed, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return touchpad;
|
|
||||||
})();
|
|
||||||
|
|
||||||
var keyboard = (function() {
|
|
||||||
var keyboard = {};
|
|
||||||
|
|
||||||
keyboard.handleKeydown = function(evt) {
|
|
||||||
if (evt.ctrlKey || evt.altKey || evt.isComposing) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var key = null;
|
|
||||||
if (evt.key == "OS" || evt.key == "Super" || evt.key == "Meta") {
|
|
||||||
key = KEY_SUPER;
|
|
||||||
} else if (evt.key == "Backspace") {
|
|
||||||
key = KEY_BACK_SPACE;
|
|
||||||
} else if (evt.key == "Enter") {
|
|
||||||
key = KEY_RETURN;
|
|
||||||
} else if (evt.key == "Delete") {
|
|
||||||
key = KEY_DELETE;
|
|
||||||
} else if (evt.key == "Home") {
|
|
||||||
key = KEY_HOME;
|
|
||||||
} else if (evt.key == "End") {
|
|
||||||
key = KEY_END;
|
|
||||||
} else if (evt.key == "Left" || evt.key == "ArrowLeft") {
|
|
||||||
key = KEY_LEFT;
|
|
||||||
} else if (evt.key == "Right" || evt.key == "ArrowRight") {
|
|
||||||
key = KEY_RIGHT;
|
|
||||||
} else if (evt.key == "Up" || evt.key == "ArrowUp") {
|
|
||||||
key = KEY_UP;
|
|
||||||
} else if (evt.key == "Down" || evt.key == "ArrowDown") {
|
|
||||||
key = KEY_DOWN;
|
|
||||||
}
|
|
||||||
if (key != null) {
|
|
||||||
if (!evt.shiftKey) {
|
|
||||||
evt.preventDefault();
|
|
||||||
controller.keyboardKey(key);
|
|
||||||
}
|
|
||||||
} else if (evt.key.length == 1) {
|
|
||||||
evt.preventDefault();
|
|
||||||
controller.keyboardText(evt.key);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return keyboard;
|
|
||||||
})();
|
|
||||||
|
|
||||||
var mouse = (function() {
|
|
||||||
var mouse = {};
|
|
||||||
|
|
||||||
var buttons = 0;
|
|
||||||
|
|
||||||
function updateButtons(newButtons) {
|
|
||||||
for (var button = 0; button < 3; button += 1) {
|
|
||||||
var flag = 1 << button;
|
|
||||||
if ((newButtons&flag) != (buttons&flag)) {
|
|
||||||
controller.pointerButton(button, newButtons&flag);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
buttons = newButtons;
|
|
||||||
}
|
|
||||||
|
|
||||||
mouse.handleMousedown = mouse.handleMouseup = function(evt) {
|
|
||||||
updateButtons(evt.buttons);
|
|
||||||
};
|
|
||||||
|
|
||||||
mouse.handleMousemove = function(evt) {
|
|
||||||
controller.pointerMove(evt.movementX*config.mouseMoveSpeed, evt.movementY*config.mouseMoveSpeed);
|
|
||||||
};
|
|
||||||
|
|
||||||
mouse.handleWheel = function(evt) {
|
|
||||||
if (evt.deltaMode == WheelEvent.DOM_DELTA_PIXEL) {
|
|
||||||
controller.pointerScroll(evt.deltaX*config.mouseScrollSpeed, evt.deltaY*config.mouseScrollSpeed, true);
|
|
||||||
} else if (evt.deltaMode == WheelEvent.DOM_DELTA_LINE) {
|
|
||||||
controller.pointerScroll(evt.deltaX*20*config.mouseScrollSpeed, evt.deltaY*20*config.mouseScrollSpeed, true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return mouse;
|
|
||||||
})();
|
|
||||||
|
|
||||||
function challengeResponse(message) {
|
|
||||||
var shaObj = new jsSHA("SHA-256", "TEXT");
|
|
||||||
shaObj.setHMACKey(message, "TEXT");
|
|
||||||
shaObj.update(window.location.hash.substr(1));
|
|
||||||
return btoa(shaObj.getHMAC("BYTES"));
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener("load", function() {
|
|
||||||
var DEFAULT_KEYS_SCENE = {
|
|
||||||
"": 0,
|
|
||||||
"keyboard": 1
|
|
||||||
};
|
|
||||||
var ready = false;
|
|
||||||
var closed = false;
|
|
||||||
var scenes = document.querySelectorAll("body > .scene");
|
|
||||||
var openingScene = document.getElementById("opening");
|
|
||||||
var closedScene = document.getElementById("closed");
|
|
||||||
var padScene = document.getElementById("pad");
|
|
||||||
var keysScene = document.getElementById("keys");
|
|
||||||
var keysSubScenes = keysScene.querySelectorAll(".scene");
|
|
||||||
var keyboardScene = document.getElementById("keyboard");
|
|
||||||
var fullscreenbutton = document.getElementById("fullscreenbutton");
|
|
||||||
var keyboardTextarea = keyboardScene.querySelector("textarea");
|
|
||||||
var mouseScene = document.getElementById("mouse");
|
|
||||||
var activeScene = null;
|
|
||||||
var keysActiveName = "";
|
|
||||||
|
|
||||||
function showScene(scene) {
|
|
||||||
activeScene = scene;
|
|
||||||
if (util.fullscreenElement() && !scene.classList.contains("fullscreen")) {
|
|
||||||
util.exitFullscreen();
|
|
||||||
}
|
|
||||||
if (util.pointerLockElement() && activeScene != mouseScene) {
|
|
||||||
util.exitPointerLock();
|
|
||||||
}
|
|
||||||
keyboardTextarea.value = "";
|
|
||||||
scenes.forEach(function(otherScene) {
|
|
||||||
otherScene.classList.toggle("hidden", otherScene != scene);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function showKeysScene(index) {
|
|
||||||
if (!Number.isInteger(index) || index < 0 || keysSubScenes.length <= index) {
|
|
||||||
index = 0;
|
|
||||||
}
|
|
||||||
sessionStorage.setItem(keysActiveName, index);
|
|
||||||
for (var i = 0; i < keysSubScenes.length; i += 1) {
|
|
||||||
keysSubScenes[i].classList.toggle("hidden", i != index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function showKeys(name) {
|
|
||||||
showScene(keysScene);
|
|
||||||
keysActiveName = "keys" + (name ? ":" + name : "");
|
|
||||||
var keysIndex = parseInt(sessionStorage.getItem(keysActiveName));
|
|
||||||
if (isNaN(keysIndex)) {
|
|
||||||
keysIndex = DEFAULT_KEYS_SCENE[name || ""] || 0;
|
|
||||||
}
|
|
||||||
showKeysScene(keysIndex);
|
|
||||||
if (history.state != keysActiveName) {
|
|
||||||
history.pushState(keysActiveName, "");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function showKeyboard() {
|
|
||||||
showScene(keyboardScene);
|
|
||||||
keyboardTextarea.value = sessionStorage.getItem("keyboard") || "";
|
|
||||||
keyboardTextarea.focus();
|
|
||||||
if (history.state != "keyboard") {
|
|
||||||
history.pushState("keyboard", "");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
keyboardTextarea.oninput = function() {
|
|
||||||
sessionStorage.setItem("keyboard", keyboardTextarea.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
function updateUI() {
|
|
||||||
if (!ready) {
|
|
||||||
showScene(closed ? closedScene : openingScene);
|
|
||||||
} else if (util.pointerLockElement()) {
|
|
||||||
showScene(mouseScene);
|
|
||||||
} else if ((history.state || "").split(":")[0] == "keys") {
|
|
||||||
showKeys(history.state.substr("keys:".length));
|
|
||||||
} else if (history.state == "keyboard") {
|
|
||||||
showKeyboard();
|
|
||||||
} else {
|
|
||||||
showScene(padScene);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateUI();
|
|
||||||
|
|
||||||
var wsURL = new URL("ws", location.href);
|
|
||||||
wsURL.protocol = wsURL.protocol == "http:" ? "ws:" : "wss:";
|
|
||||||
ws = new WebSocket(wsURL);
|
|
||||||
|
|
||||||
var authenticated = false;
|
|
||||||
ws.onmessage = function(evt) {
|
|
||||||
if (!authenticated) {
|
|
||||||
ws.send(challengeResponse(evt.data));
|
|
||||||
authenticated = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
config = JSON.parse(evt.data);
|
|
||||||
} catch (e) {
|
|
||||||
ws.close();
|
|
||||||
throw (e);
|
|
||||||
}
|
|
||||||
ready = true;
|
|
||||||
updateUI();
|
|
||||||
};
|
|
||||||
|
|
||||||
ws.onclose = function() {
|
|
||||||
ready = false;
|
|
||||||
closed = true;
|
|
||||||
updateUI();
|
|
||||||
};
|
|
||||||
|
|
||||||
document.getElementById("keysbutton").addEventListener("click", function() {
|
|
||||||
showKeys();
|
|
||||||
});
|
|
||||||
document.getElementById("keyboardbutton").addEventListener("click", function() {
|
|
||||||
showKeyboard();
|
|
||||||
});
|
|
||||||
util.addFullscreenchangeEventListener(function() {
|
|
||||||
updateUI();
|
|
||||||
});
|
|
||||||
if (!util.fullscreenEnabled()) {
|
|
||||||
fullscreenbutton.classList.add("hidden");
|
|
||||||
}
|
|
||||||
fullscreenbutton.addEventListener("click", function() {
|
|
||||||
if (util.fullscreenElement()) {
|
|
||||||
util.exitFullscreen();
|
|
||||||
} else {
|
|
||||||
util.requestFullscreen(document.documentElement, {navigationUI: "hide"});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
document.getElementById("switchbutton").addEventListener("click", function() {
|
|
||||||
var keysIndex = 0;
|
|
||||||
for (var i = 0; i < keysSubScenes.length; i += 1) {
|
|
||||||
if (!keysSubScenes[i].classList.contains("hidden")) {
|
|
||||||
keysIndex = i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
showKeysScene(keysIndex + 1);
|
|
||||||
});
|
|
||||||
[
|
|
||||||
{id: "browserbackbutton", key: KEY_BROWSER_BACK},
|
|
||||||
{id: "superbutton", key: KEY_SUPER},
|
|
||||||
{id: "browserforwardbutton", key: KEY_BROWSER_FORWARD},
|
|
||||||
{id: "prevtrackbutton", key: KEY_MEDIA_PREV_TRACK},
|
|
||||||
{id: "playpausebutton", key: KEY_MEDIA_PLAY_PAUSE},
|
|
||||||
{id: "nexttrackbutton", key: KEY_MEDIA_NEXT_TRACK},
|
|
||||||
{id: "volumedownbutton", key: KEY_VOLUME_DOWN},
|
|
||||||
{id: "volumemutebutton", key: KEY_VOLUME_MUTE},
|
|
||||||
{id: "volumeupbutton", key: KEY_VOLUME_UP},
|
|
||||||
{id: "backspacebutton", key: KEY_BACK_SPACE},
|
|
||||||
{id: "returnbutton", key: KEY_RETURN},
|
|
||||||
{id: "deletebutton", key: KEY_DELETE},
|
|
||||||
{id: "homebutton", key: KEY_HOME},
|
|
||||||
{id: "endbutton", key: KEY_END},
|
|
||||||
{id: "leftbutton", key: KEY_LEFT},
|
|
||||||
{id: "rightbutton", key: KEY_RIGHT},
|
|
||||||
{id: "upbutton", key: KEY_UP},
|
|
||||||
{id: "downbutton", key: KEY_DOWN}
|
|
||||||
].forEach(function(o) {
|
|
||||||
document.getElementById(o.id).addEventListener("click", function() {
|
|
||||||
controller.keyboardKey(o.key);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
document.getElementById("keyboardkeysbutton").addEventListener("click", function() {
|
|
||||||
showKeys("keyboard");
|
|
||||||
});
|
|
||||||
document.getElementById("sendbutton").addEventListener("click", function() {
|
|
||||||
if (keyboardTextarea.value) {
|
|
||||||
// normalize line endings
|
|
||||||
controller.keyboardText(keyboardTextarea.value.replace(/\r\n?/g, "\n"));
|
|
||||||
keyboardTextarea.value = "";
|
|
||||||
keyboardTextarea.oninput();
|
|
||||||
}
|
|
||||||
history.back();
|
|
||||||
});
|
|
||||||
window.addEventListener("popstate", function() {
|
|
||||||
updateUI();
|
|
||||||
});
|
|
||||||
document.getElementById("reloadbutton").addEventListener("click", function() {
|
|
||||||
location.reload();
|
|
||||||
});
|
|
||||||
document.querySelectorAll(".backbutton").forEach(function(button) {
|
|
||||||
button.addEventListener("click", function() {
|
|
||||||
history.back();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
document.addEventListener("touchstart", touchpad.handleTouchstart);
|
|
||||||
document.addEventListener("touchend", touchpad.handleTouchend);
|
|
||||||
document.addEventListener("touchcancel", touchpad.handleTouchcancel);
|
|
||||||
document.addEventListener("touchmove", touchpad.handleTouchmove);
|
|
||||||
document.addEventListener("keydown", function(evt) {
|
|
||||||
if (activeScene && activeScene.classList.contains("key")) {
|
|
||||||
keyboard.handleKeydown(evt);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
util.addPointerlockchangeEventListener(function() {
|
|
||||||
updateUI();
|
|
||||||
});
|
|
||||||
document.addEventListener("mousedown", function(event) {
|
|
||||||
if (activeScene != mouseScene && event.buttons == 1 && event.target.classList.contains("touch")) {
|
|
||||||
util.requestPointerLock(mouseScene);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
["touchstart", "touchend", "touchcancel", "touchmove"].forEach(function(type) {
|
|
||||||
mouseScene.addEventListener(type, function(evt) {
|
|
||||||
evt.preventDefault();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
mouseScene.addEventListener("mousedown", mouse.handleMousedown);
|
|
||||||
mouseScene.addEventListener("mouseup", mouse.handleMouseup);
|
|
||||||
mouseScene.addEventListener("mousemove", mouse.handleMousemove);
|
|
||||||
mouseScene.addEventListener("wheel", mouse.handleWheel);
|
|
||||||
});
|
|
||||||
|
|
@ -1,10 +1,8 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<meta charset="utf-8">
|
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no, interactive-widget=resizes-content">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=0">
|
<script src="app/main.mjs" type="module"></script>
|
||||||
<script src="fn.js"></script>
|
|
||||||
<script src="sha256.js"></script>
|
|
||||||
<title>Remote Touchpad</title>
|
<title>Remote Touchpad</title>
|
||||||
<link href="main.css" media="screen" rel="stylesheet">
|
<link href="main.css" media="screen" rel="stylesheet">
|
||||||
<link href="icon.png" type="image/png" rel="icon">
|
<link href="icon.png" type="image/png" rel="icon">
|
||||||
|
|
@ -13,61 +11,90 @@
|
||||||
<noscript><p>JavaScript is required!</p></noscript>
|
<noscript><p>JavaScript is required!</p></noscript>
|
||||||
<p>Connecting…</p>
|
<p>Connecting…</p>
|
||||||
</div>
|
</div>
|
||||||
<div id="closed" class="scene hidden">
|
|
||||||
|
<div id="closed" class="scene">
|
||||||
<p>Disconnected</p>
|
<p>Disconnected</p>
|
||||||
<button id="reloadbutton">↻</button>
|
<button class="large" onclick="location.reload()">↻</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="pad" class="scene touch key fullscreen hidden">
|
|
||||||
<p class="touch">Touchpad</p>
|
<div id="pad" class="scene touch-input mouse-input keyboard-input allow-fullscreen">
|
||||||
<button id="keysbutton">≡</button>
|
<p class="background">Touchpad</p>
|
||||||
<button id="fullscreenbutton">⤢</button>
|
<button class="top left" onclick="app.showKeys()">≡</button>
|
||||||
<button id="keyboardbutton">⌨</button>
|
<button class="bottom left visible-if-fullscreen-enabled" onclick="app.toggleFullscreen()">⤢</button>
|
||||||
|
<button class="bottom right" onclick="app.showTextInput()">⌨</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="keyboard" class="scene hidden">
|
|
||||||
<textarea cols=1 rows=1></textarea>
|
<div id="text-input" class="scene">
|
||||||
|
<textarea class="grow" cols=1 rows=1></textarea>
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<button id="keyboardkeysbutton">≡</button>
|
<button onclick="app.showKeys('text-input', 1)">≡</button>
|
||||||
<button id="sendbutton">➣</button>
|
<button class="grow" id="send-text" data-send-content="➣" data-back-content="⯇"></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="keys" class="scene touch key fullscreen hidden">
|
|
||||||
<div class="scene touch">
|
<div id="keys" class="scene touch-input mouse-input keyboard-input allow-fullscreen">
|
||||||
<div class="buttons">
|
<div class="page">
|
||||||
<button id="browserbackbutton">⭠</button>
|
<div class="buttons" style="grid-column: 1 / 4">
|
||||||
<button id="superbutton">🪟</button>
|
<button onclick="app.key(app.KEY_BROWSER_BACK)">⭠</button>
|
||||||
<button id="browserforwardbutton">⭢</button>
|
<button onclick="app.key(app.KEY_SUPER)">🪟</button>
|
||||||
|
<button onclick="app.key(app.KEY_BROWSER_FORWARD)">⭢</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="buttons">
|
<div class="buttons" style="grid-column: 1 / 4">
|
||||||
<button id="prevtrackbutton">⏮</button>
|
<button onclick="app.key(app.KEY_MEDIA_PREV_TRACK)">⏮</button>
|
||||||
<button id="playpausebutton">⏯</button>
|
<button onclick="app.key(app.KEY_MEDIA_PLAY_PAUSE)">⏯</button>
|
||||||
<button id="nexttrackbutton">⏭</button>
|
<button onclick="app.key(app.KEY_MEDIA_NEXT_TRACK)">⏭</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="buttons">
|
<div class="buttons" style="grid-column: 1 / 4">
|
||||||
<button id="volumedownbutton">🔉</button>
|
<button onclick="app.key(app.KEY_VOLUME_DOWN)">🔉</button>
|
||||||
<button id="volumemutebutton">🔇</button>
|
<button onclick="app.key(app.KEY_VOLUME_MUTE)">🔇</button>
|
||||||
<button id="volumeupbutton">🔊</button>
|
<button onclick="app.key(app.KEY_VOLUME_UP)">🔊</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="scene touch">
|
<div class="page">
|
||||||
<div class="buttons">
|
<div class="buttons" style="grid-column: 1 / 4">
|
||||||
<button id="backspacebutton">⌫</button>
|
<button onclick="app.key(app.KEY_BACK_SPACE)">⌫</button>
|
||||||
<button id="returnbutton">⮠</button>
|
<button onclick="app.key(app.KEY_RETURN)">⮠</button>
|
||||||
<button id="deletebutton">⌦</button>
|
<button onclick="app.key(app.KEY_DELETE)">⌦</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="buttons">
|
<div class="buttons" style="grid-column: 1 / 4">
|
||||||
<button id="homebutton">⭰</button>
|
<button onclick="app.key(app.KEY_HOME)">⭰</button>
|
||||||
<button id="upbutton">↑</button>
|
<button onclick="app.key(app.KEY_UP)">↑</button>
|
||||||
<button id="endbutton">⭲</button>
|
<button onclick="app.key(app.KEY_END)">⭲</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="buttons">
|
<div class="buttons" style="grid-column: 1 / 4">
|
||||||
<button id="leftbutton">←</button>
|
<button onclick="app.key(app.KEY_LEFT)">←</button>
|
||||||
<button id="downbutton">↓</button>
|
<button onclick="app.key(app.KEY_DOWN)">↓</button>
|
||||||
<button id="rightbutton">→</button>
|
<button onclick="app.key(app.KEY_RIGHT)">→</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button id="switchbutton">⮂</button>
|
<div class="page">
|
||||||
<button class="backbutton">⯇</button>
|
<button onclick="app.key(app.KEY_BACK_SPACE)">⌫</button>
|
||||||
|
<button onclick="app.text(this.textContent)">/</button>
|
||||||
|
<button onclick="app.text(this.textContent)">*</button>
|
||||||
|
<button onclick="app.text(this.textContent)">-</button>
|
||||||
|
|
||||||
|
<button style="grid-row: 2" onclick="app.text(this.textContent)">7</button>
|
||||||
|
<button onclick="app.text(this.textContent)">8</button>
|
||||||
|
<button onclick="app.text(this.textContent)">9</button>
|
||||||
|
<button onclick="app.text(this.textContent)">+</button>
|
||||||
|
|
||||||
|
<button style="grid-row: 3" onclick="app.text(this.textContent)">4</button>
|
||||||
|
<button onclick="app.text(this.textContent)">5</button>
|
||||||
|
<button onclick="app.text(this.textContent)">6</button>
|
||||||
|
<button onclick="app.text(this.textContent)">=</button>
|
||||||
|
|
||||||
|
<button style="grid-row: 4" onclick="app.text(this.textContent)">1</button>
|
||||||
|
<button onclick="app.text(this.textContent)">2</button>
|
||||||
|
<button onclick="app.text(this.textContent)">3</button>
|
||||||
|
<button style="grid-row: 4 / 6; grid-column: 4" onclick="app.key(app.KEY_RETURN)">⮠</button>
|
||||||
|
|
||||||
|
<button style="grid-row: 5; grid-column: 1 / 3" onclick="app.text(this.textContent)">0</button>
|
||||||
|
<button onclick="app.text(this.textContent)"><script>document.write(Intl.NumberFormat().formatToParts(1.1).find(x => x.type === "decimal").value)</script></button>
|
||||||
</div>
|
</div>
|
||||||
<div id="mouse" class="scene key fullscreen hidden">
|
<button class="top right" onclick="app.setKeysPage(1, true)">⮂</button>
|
||||||
<p>Mouse</p>
|
<button class="top left" onclick="history.back()">⯇</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="mouse" class="scene keyboard-input allow-fullscreen">
|
||||||
|
<p class="background">Mouse</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
248
webdata/main.css
248
webdata/main.css
|
|
@ -17,6 +17,7 @@ html {
|
||||||
color: white;
|
color: white;
|
||||||
background-image: url(tex.png);
|
background-image: url(tex.png);
|
||||||
touch-action: none;
|
touch-action: none;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
html,
|
html,
|
||||||
|
|
@ -33,6 +34,9 @@ body,
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2rem;
|
||||||
|
padding: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
|
|
@ -42,42 +46,7 @@ p {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
p.background {
|
||||||
padding: 0;
|
|
||||||
background-color: transparent;
|
|
||||||
border: 1px solid black;
|
|
||||||
height: 1.5em;
|
|
||||||
width: 2em;
|
|
||||||
border-radius: 2px;
|
|
||||||
font-size: 2rem;
|
|
||||||
color: white;
|
|
||||||
font-family: "icon";
|
|
||||||
overflow: hidden;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:focus {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#opening,
|
|
||||||
#closed {
|
|
||||||
padding: 0 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#opening noscript p,
|
|
||||||
#closed p {
|
|
||||||
margin-bottom: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
#reloadbutton {
|
|
||||||
font-size: 4rem;
|
|
||||||
margin: 0 auto;
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#pad > p,
|
|
||||||
#mouse > p {
|
|
||||||
color: gray;
|
color: gray;
|
||||||
text-shadow:
|
text-shadow:
|
||||||
-1px -1px 0 black,
|
-1px -1px 0 black,
|
||||||
|
|
@ -87,44 +56,94 @@ button:focus {
|
||||||
opacity: 0.08;
|
opacity: 0.08;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 4rem;
|
font-size: 4rem;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 32rem) {
|
@media (max-width: 32rem) {
|
||||||
#pad > p,
|
p.background {
|
||||||
#mouse > p {
|
|
||||||
font-size: 13vw;
|
font-size: 13vw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#pad > button, #keys > button {
|
.large {
|
||||||
|
font-size: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 0;
|
||||||
|
background-color: transparent;
|
||||||
|
border: 1px solid black;
|
||||||
|
height: 1.5em;
|
||||||
|
width: 2em;
|
||||||
|
min-width: 1.2em;
|
||||||
|
min-height: 1.2em;
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
border-radius: 2px;
|
||||||
|
font-size: 2rem;
|
||||||
|
color: white;
|
||||||
|
font-family: "icon";
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: box-shadow 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.click {
|
||||||
|
box-shadow: 0 0 1rem white inset;
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.top,
|
||||||
|
button.bottom,
|
||||||
|
button.left,
|
||||||
|
button.right {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
max-width: 50%;
|
max-width: 50%;
|
||||||
max-height: 50%;
|
max-height: 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#fullscreenbutton {
|
button.top {
|
||||||
left: 0;
|
|
||||||
bottom: 0;
|
|
||||||
border-bottom: none;
|
|
||||||
border-left: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#keyboardbutton {
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
border-right: none;
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#keysbutton {
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
top: 0;
|
||||||
border-top: none;
|
border-top: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.bottom {
|
||||||
|
bottom: 0;
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.left {
|
||||||
|
left: 0;
|
||||||
border-left: none;
|
border-left: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button.right {
|
||||||
|
right: 0;
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
min-height: 0;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons > button:not(:last-child) {
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene > .grow,
|
||||||
|
.buttons > .grow {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
textarea {
|
textarea {
|
||||||
flex-grow: 1;
|
|
||||||
border: none;
|
border: none;
|
||||||
resize: none;
|
resize: none;
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
|
|
@ -134,77 +153,76 @@ textarea {
|
||||||
padding: 0 0.5rem;
|
padding: 0 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
#keyboard .buttons {
|
textarea:focus {
|
||||||
display: flex;
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#keys {
|
||||||
|
padding: 0;
|
||||||
|
justify-content: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
#keys .page {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.5rem;
|
||||||
|
min-height: 0;
|
||||||
|
min-width: 0;
|
||||||
|
grid-auto-rows: minmax(min-content, 6rem);
|
||||||
|
grid-auto-columns: minmax(min-content, 8rem);
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#keys .page * {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#text-input {
|
||||||
|
align-items: stretch;
|
||||||
|
padding: 0;
|
||||||
|
gap: 0;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
}
|
}
|
||||||
|
|
||||||
#keyboard .buttons > button {
|
#text-input .buttons {
|
||||||
border-bottom: none;
|
|
||||||
border-left: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#keyboard .buttons > button:last-of-type {
|
|
||||||
flex-grow: 1;
|
|
||||||
border-right: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-aspect-ratio: 4/3) {
|
|
||||||
#keyboard {
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
|
|
||||||
#keyboard .buttons {
|
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
#keyboard .buttons > button {
|
#text-input .buttons > button {
|
||||||
border-top: none;
|
border-top: none;
|
||||||
border-right: none;
|
border-right: none;
|
||||||
border-left: 1px solid black;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#keyboard .buttons > button:last-of-type {
|
#text-input .buttons > button:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Only Safari for iOS */
|
||||||
|
@supports (-webkit-touch-callout: none) {
|
||||||
|
/* Virtual keyboard can overlay content */
|
||||||
|
#text-input .buttons > button:last-child {
|
||||||
|
display: flex;
|
||||||
|
padding-top: 0.2em;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Portrait orientation (not for Safari on iOS) */
|
||||||
|
@supports not (-webkit-touch-callout: none) { @media (max-aspect-ratio: 4/3) {
|
||||||
|
#text-input {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
#text-input .buttons {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
#text-input .buttons > button {
|
||||||
border-top: 1px solid black;
|
border-top: 1px solid black;
|
||||||
}
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#keys .scene {
|
#text-input .buttons > button:first-child {
|
||||||
padding: 0.5rem;
|
|
||||||
padding-bottom: 0;
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
#switchbutton {
|
|
||||||
right: 0;
|
|
||||||
top: 0;
|
|
||||||
border-top: none;
|
|
||||||
border-right: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#keys .backbutton {
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
border-top: none;
|
|
||||||
border-left: none;
|
border-left: none;
|
||||||
}
|
}
|
||||||
|
} }
|
||||||
#keys .buttons {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
flex-shrink: 1;
|
|
||||||
height: 6rem;
|
|
||||||
min-height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#keys .buttons > button:not(:last-of-type) {
|
|
||||||
border-right: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#keys .buttons > button {
|
|
||||||
flex-shrink: 1;
|
|
||||||
height: 100%;
|
|
||||||
width: 8rem;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
/*
|
|
||||||
A JavaScript implementation of the SHA family of hashes, as
|
|
||||||
defined in FIPS PUB 180-4 and FIPS PUB 202, as well as the corresponding
|
|
||||||
HMAC implementation as defined in FIPS PUB 198a
|
|
||||||
|
|
||||||
Copyright 2008-2018 Brian Turek, 1998-2009 Paul Johnston & Contributors
|
|
||||||
Distributed under the BSD License
|
|
||||||
See http://caligatio.github.com/jsSHA/ for more information
|
|
||||||
*/
|
|
||||||
'use strict';(function(I){function w(c,a,d){var l=0,b=[],g=0,f,n,k,e,h,q,y,p,m=!1,t=[],r=[],u,z=!1;d=d||{};f=d.encoding||"UTF8";u=d.numRounds||1;if(u!==parseInt(u,10)||1>u)throw Error("numRounds must a integer >= 1");if(0===c.lastIndexOf("SHA-",0))if(q=function(b,a){return A(b,a,c)},y=function(b,a,l,f){var g,e;if("SHA-224"===c||"SHA-256"===c)g=(a+65>>>9<<4)+15,e=16;else throw Error("Unexpected error in SHA-2 implementation");for(;b.length<=g;)b.push(0);b[a>>>5]|=128<<24-a%32;a=a+l;b[g]=a&4294967295;
|
|
||||||
b[g-1]=a/4294967296|0;l=b.length;for(a=0;a<l;a+=e)f=A(b.slice(a,a+e),f,c);if("SHA-224"===c)b=[f[0],f[1],f[2],f[3],f[4],f[5],f[6]];else if("SHA-256"===c)b=f;else throw Error("Unexpected error in SHA-2 implementation");return b},p=function(b){return b.slice()},"SHA-224"===c)h=512,e=224;else if("SHA-256"===c)h=512,e=256;else throw Error("Chosen SHA variant is not supported");else throw Error("Chosen SHA variant is not supported");k=B(a,f);n=x(c);this.setHMACKey=function(b,a,g){var e;if(!0===m)throw Error("HMAC key already set");
|
|
||||||
if(!0===z)throw Error("Cannot set HMAC key after calling update");f=(g||{}).encoding||"UTF8";a=B(a,f)(b);b=a.binLen;a=a.value;e=h>>>3;g=e/4-1;if(e<b/8){for(a=y(a,b,0,x(c));a.length<=g;)a.push(0);a[g]&=4294967040}else if(e>b/8){for(;a.length<=g;)a.push(0);a[g]&=4294967040}for(b=0;b<=g;b+=1)t[b]=a[b]^909522486,r[b]=a[b]^1549556828;n=q(t,n);l=h;m=!0};this.update=function(a){var c,f,e,d=0,p=h>>>5;c=k(a,b,g);a=c.binLen;f=c.value;c=a>>>5;for(e=0;e<c;e+=p)d+h<=a&&(n=q(f.slice(e,e+p),n),d+=h);l+=d;b=f.slice(d>>>
|
|
||||||
5);g=a%h;z=!0};this.getHash=function(a,f){var d,h,k,q;if(!0===m)throw Error("Cannot call getHash after setting HMAC key");k=C(f);switch(a){case "HEX":d=function(a){return D(a,e,k)};break;case "B64":d=function(a){return E(a,e,k)};break;case "BYTES":d=function(a){return F(a,e)};break;case "ARRAYBUFFER":try{h=new ArrayBuffer(0)}catch(v){throw Error("ARRAYBUFFER not supported by this environment");}d=function(a){return G(a,e)};break;default:throw Error("format must be HEX, B64, BYTES, or ARRAYBUFFER");
|
|
||||||
}q=y(b.slice(),g,l,p(n));for(h=1;h<u;h+=1)q=y(q,e,0,x(c));return d(q)};this.getHMAC=function(a,f){var d,k,t,u;if(!1===m)throw Error("Cannot call getHMAC without first setting HMAC key");t=C(f);switch(a){case "HEX":d=function(a){return D(a,e,t)};break;case "B64":d=function(a){return E(a,e,t)};break;case "BYTES":d=function(a){return F(a,e)};break;case "ARRAYBUFFER":try{d=new ArrayBuffer(0)}catch(v){throw Error("ARRAYBUFFER not supported by this environment");}d=function(a){return G(a,e)};break;default:throw Error("outputFormat must be HEX, B64, BYTES, or ARRAYBUFFER");
|
|
||||||
}k=y(b.slice(),g,l,p(n));u=q(r,x(c));u=y(k,e,h,u);return d(u)}}function m(){}function D(c,a,d){var l="";a/=8;var b,g;for(b=0;b<a;b+=1)g=c[b>>>2]>>>8*(3+b%4*-1),l+="0123456789abcdef".charAt(g>>>4&15)+"0123456789abcdef".charAt(g&15);return d.outputUpper?l.toUpperCase():l}function E(c,a,d){var l="",b=a/8,g,f,n;for(g=0;g<b;g+=3)for(f=g+1<b?c[g+1>>>2]:0,n=g+2<b?c[g+2>>>2]:0,n=(c[g>>>2]>>>8*(3+g%4*-1)&255)<<16|(f>>>8*(3+(g+1)%4*-1)&255)<<8|n>>>8*(3+(g+2)%4*-1)&255,f=0;4>f;f+=1)8*g+6*f<=a?l+="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".charAt(n>>>
|
|
||||||
6*(3-f)&63):l+=d.b64Pad;return l}function F(c,a){var d="",l=a/8,b,g;for(b=0;b<l;b+=1)g=c[b>>>2]>>>8*(3+b%4*-1)&255,d+=String.fromCharCode(g);return d}function G(c,a){var d=a/8,l,b=new ArrayBuffer(d),g;g=new Uint8Array(b);for(l=0;l<d;l+=1)g[l]=c[l>>>2]>>>8*(3+l%4*-1)&255;return b}function C(c){var a={outputUpper:!1,b64Pad:"=",shakeLen:-1};c=c||{};a.outputUpper=c.outputUpper||!1;!0===c.hasOwnProperty("b64Pad")&&(a.b64Pad=c.b64Pad);if("boolean"!==typeof a.outputUpper)throw Error("Invalid outputUpper formatting option");
|
|
||||||
if("string"!==typeof a.b64Pad)throw Error("Invalid b64Pad formatting option");return a}function B(c,a){var d;switch(a){case "UTF8":case "UTF16BE":case "UTF16LE":break;default:throw Error("encoding must be UTF8, UTF16BE, or UTF16LE");}switch(c){case "HEX":d=function(a,b,c){var f=a.length,d,k,e,h,q;if(0!==f%2)throw Error("String of HEX type must be in byte increments");b=b||[0];c=c||0;q=c>>>3;for(d=0;d<f;d+=2){k=parseInt(a.substr(d,2),16);if(isNaN(k))throw Error("String of HEX type contains invalid characters");
|
|
||||||
h=(d>>>1)+q;for(e=h>>>2;b.length<=e;)b.push(0);b[e]|=k<<8*(3+h%4*-1)}return{value:b,binLen:4*f+c}};break;case "TEXT":d=function(c,b,d){var f,n,k=0,e,h,q,m,p,r;b=b||[0];d=d||0;q=d>>>3;if("UTF8"===a)for(r=3,e=0;e<c.length;e+=1)for(f=c.charCodeAt(e),n=[],128>f?n.push(f):2048>f?(n.push(192|f>>>6),n.push(128|f&63)):55296>f||57344<=f?n.push(224|f>>>12,128|f>>>6&63,128|f&63):(e+=1,f=65536+((f&1023)<<10|c.charCodeAt(e)&1023),n.push(240|f>>>18,128|f>>>12&63,128|f>>>6&63,128|f&63)),h=0;h<n.length;h+=1){p=k+
|
|
||||||
q;for(m=p>>>2;b.length<=m;)b.push(0);b[m]|=n[h]<<8*(r+p%4*-1);k+=1}else if("UTF16BE"===a||"UTF16LE"===a)for(r=2,n="UTF16LE"===a&&!0||"UTF16LE"!==a&&!1,e=0;e<c.length;e+=1){f=c.charCodeAt(e);!0===n&&(h=f&255,f=h<<8|f>>>8);p=k+q;for(m=p>>>2;b.length<=m;)b.push(0);b[m]|=f<<8*(r+p%4*-1);k+=2}return{value:b,binLen:8*k+d}};break;case "B64":d=function(a,b,c){var f=0,d,k,e,h,q,m,p;if(-1===a.search(/^[a-zA-Z0-9=+\/]+$/))throw Error("Invalid character in base-64 string");k=a.indexOf("=");a=a.replace(/\=/g,
|
|
||||||
"");if(-1!==k&&k<a.length)throw Error("Invalid '=' found in base-64 string");b=b||[0];c=c||0;m=c>>>3;for(k=0;k<a.length;k+=4){q=a.substr(k,4);for(e=h=0;e<q.length;e+=1)d="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".indexOf(q[e]),h|=d<<18-6*e;for(e=0;e<q.length-1;e+=1){p=f+m;for(d=p>>>2;b.length<=d;)b.push(0);b[d]|=(h>>>16-8*e&255)<<8*(3+p%4*-1);f+=1}}return{value:b,binLen:8*f+c}};break;case "BYTES":d=function(a,b,c){var d,n,k,e,h;b=b||[0];c=c||0;k=c>>>3;for(n=0;n<a.length;n+=
|
|
||||||
1)d=a.charCodeAt(n),h=n+k,e=h>>>2,b.length<=e&&b.push(0),b[e]|=d<<8*(3+h%4*-1);return{value:b,binLen:8*a.length+c}};break;case "ARRAYBUFFER":try{d=new ArrayBuffer(0)}catch(l){throw Error("ARRAYBUFFER not supported by this environment");}d=function(a,b,c){var d,n,k,e,h;b=b||[0];c=c||0;n=c>>>3;h=new Uint8Array(a);for(d=0;d<a.byteLength;d+=1)e=d+n,k=e>>>2,b.length<=k&&b.push(0),b[k]|=h[d]<<8*(3+e%4*-1);return{value:b,binLen:8*a.byteLength+c}};break;default:throw Error("format must be HEX, TEXT, B64, BYTES, or ARRAYBUFFER");
|
|
||||||
}return d}function r(c,a){return c>>>a|c<<32-a}function J(c,a,d){return c&a^~c&d}function K(c,a,d){return c&a^c&d^a&d}function L(c){return r(c,2)^r(c,13)^r(c,22)}function M(c){return r(c,6)^r(c,11)^r(c,25)}function N(c){return r(c,7)^r(c,18)^c>>>3}function O(c){return r(c,17)^r(c,19)^c>>>10}function P(c,a){var d=(c&65535)+(a&65535);return((c>>>16)+(a>>>16)+(d>>>16)&65535)<<16|d&65535}function Q(c,a,d,l){var b=(c&65535)+(a&65535)+(d&65535)+(l&65535);return((c>>>16)+(a>>>16)+(d>>>16)+(l>>>16)+(b>>>
|
|
||||||
16)&65535)<<16|b&65535}function R(c,a,d,l,b){var g=(c&65535)+(a&65535)+(d&65535)+(l&65535)+(b&65535);return((c>>>16)+(a>>>16)+(d>>>16)+(l>>>16)+(b>>>16)+(g>>>16)&65535)<<16|g&65535}function x(c){var a=[],d;if(0===c.lastIndexOf("SHA-",0))switch(a=[3238371032,914150663,812702999,4144912697,4290775857,1750603025,1694076839,3204075428],d=[1779033703,3144134277,1013904242,2773480762,1359893119,2600822924,528734635,1541459225],c){case "SHA-224":break;case "SHA-256":a=d;break;case "SHA-384":a=[new m,new m,
|
|
||||||
new m,new m,new m,new m,new m,new m];break;case "SHA-512":a=[new m,new m,new m,new m,new m,new m,new m,new m];break;default:throw Error("Unknown SHA variant");}else throw Error("No SHA variants supported");return a}function A(c,a,d){var l,b,g,f,n,k,e,h,m,r,p,w,t,x,u,z,A,B,C,D,E,F,v=[],G;if("SHA-224"===d||"SHA-256"===d)r=64,w=1,F=Number,t=P,x=Q,u=R,z=N,A=O,B=L,C=M,E=K,D=J,G=H;else throw Error("Unexpected error in SHA-2 implementation");d=a[0];l=a[1];b=a[2];g=a[3];f=a[4];n=a[5];k=a[6];e=a[7];for(p=
|
|
||||||
0;p<r;p+=1)16>p?(m=p*w,h=c.length<=m?0:c[m],m=c.length<=m+1?0:c[m+1],v[p]=new F(h,m)):v[p]=x(A(v[p-2]),v[p-7],z(v[p-15]),v[p-16]),h=u(e,C(f),D(f,n,k),G[p],v[p]),m=t(B(d),E(d,l,b)),e=k,k=n,n=f,f=t(g,h),g=b,b=l,l=d,d=t(h,m);a[0]=t(d,a[0]);a[1]=t(l,a[1]);a[2]=t(b,a[2]);a[3]=t(g,a[3]);a[4]=t(f,a[4]);a[5]=t(n,a[5]);a[6]=t(k,a[6]);a[7]=t(e,a[7]);return a}var H;H=[1116352408,1899447441,3049323471,3921009573,961987163,1508970993,2453635748,2870763221,3624381080,310598401,607225278,1426881987,1925078388,2162078206,
|
|
||||||
2614888103,3248222580,3835390401,4022224774,264347078,604807628,770255983,1249150122,1555081692,1996064986,2554220882,2821834349,2952996808,3210313671,3336571891,3584528711,113926993,338241895,666307205,773529912,1294757372,1396182291,1695183700,1986661051,2177026350,2456956037,2730485921,2820302411,3259730800,3345764771,3516065817,3600352804,4094571909,275423344,430227734,506948616,659060556,883997877,958139571,1322822218,1537002063,1747873779,1955562222,2024104815,2227730452,2361852424,2428436474,
|
|
||||||
2756734187,3204031479,3329325298];"function"===typeof define&&define.amd?define(function(){return w}):"undefined"!==typeof exports?("undefined"!==typeof module&&module.exports&&(module.exports=w),exports=w):I.jsSHA=w})(this);
|
|
||||||
42
webdata_test.go
Normal file
42
webdata_test.go
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 Unrud <unrud@outlook.com>
|
||||||
|
*
|
||||||
|
* This file is part of Remote-Touchpad.
|
||||||
|
*
|
||||||
|
* Remote-Touchpad is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Remote-Touchpad is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Remote-Touchpad. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestWebdataTypesCompleteness(t *testing.T) {
|
||||||
|
if err := fs.WalkDir(webdataFS, ".", func(_ string, d fs.DirEntry, err error) error {
|
||||||
|
if err != nil || d.IsDir() {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ext := filepath.Ext(d.Name())
|
||||||
|
if _, haveType := webdataTypes[ext]; !haveType {
|
||||||
|
return fmt.Errorf("missing mime type for extension %#v", ext)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue