Compare commits

...

139 commits

Author SHA1 Message Date
2ac00c3b48 Updated gitignore
Some checks failed
Test / test (linux, []) (push) Has been cancelled
Test / test (windows, [null]) (push) Has been cancelled
Test / test (linux, [null portal uinput x11]) (push) Has been cancelled
Test / test (linux, [null]) (push) Has been cancelled
Test / test (linux, [portal]) (push) Has been cancelled
Test / test (linux, [uinput]) (push) Has been cancelled
Test / test (linux, [x11]) (push) Has been cancelled
Test / test (windows, []) (push) Has been cancelled
2026-08-14 22:01:02 +02:00
ed7e8bae54 Add systemd user service and CLI wrapper for remote-touchpad
Some checks are pending
Test / test (linux, []) (push) Waiting to run
Test / test (linux, [null portal uinput x11]) (push) Waiting to run
Test / test (linux, [null]) (push) Waiting to run
Test / test (linux, [portal]) (push) Waiting to run
Test / test (linux, [uinput]) (push) Waiting to run
Test / test (linux, [x11]) (push) Waiting to run
Test / test (windows, []) (push) Waiting to run
Test / test (windows, [null]) (push) Waiting to run
This commit introduces a complete systemd user service setup for
remote-touchpad, eliminating the need for users to manually manage
the binary process. Key additions:

- New rts CLI wrapper script providing a user-friendly interface
  for service management without requiring root access
- Systemd service file for proper background operation with
  restart policies and dependency management
- Service management commands: install, start, stop, status,
  restart, and uninstall
- URL extraction and QR code generation via the url command
- Service log viewing via the log command

The installation process now:
1. Verifies Go is installed
2. Builds the remote-touchpad binary with appropriate tags
3. Installs service files to ~/.config/systemd/user/
4. Installs the binary to ~/.local/bin/
5. Enables and starts the service automatically

The README has been updated with comprehensive installation
instructions for the new user service approach.
2026-08-14 21:59:58 +02:00
Unrud
a77f3aa8ec Bump version to 1.5.4 2026-08-03 00:34:58 +02:00
Unrud
9764d1975d Update dependencies 2026-08-03 00:34:58 +02:00
Unrud
5e68074781 X11: Reduce delay when mapping new keys
Reliability is improved by XSync
2026-08-03 00:32:34 +02:00
Unrud
f80b1d6ef7 X11: Minimize mod changes 2026-08-03 00:32:34 +02:00
Unrud
346d9cf375 X11: Cleanup code 2026-08-03 00:32:33 +02:00
Unrud
6ef2be8776 Format code 2026-08-02 22:12:27 +02:00
Unrud
85b6d6ea9d X11: Use XSync to improve reliability 2026-08-02 13:10:04 +02:00
Unrud
1aa8f92cc8 Use range over integers 2026-08-02 13:10:04 +02:00
Unrud
996edb0501 Snap: Specifiy supported platforms 2026-07-11 20:59:03 +02:00
Unrud
a30bed4624 Snap: Update to core24 2026-07-11 20:20:03 +02:00
Unrud
a2dfa7ac47 Bump version to 1.5.3 2025-12-24 22:59:32 +01:00
Unrud
eebde58826 Update dependencies 2025-12-24 22:57:01 +01:00
Unrud
800171efdd Bump version to 1.5.2 2025-10-12 06:45:35 +02:00
Unrud
2b3d5c5bf3 Flatpak: Update modules.txt 2025-10-12 06:42:46 +02:00
Unrud
1add8e4cb8 Bump version to 1.5.1 2025-10-12 06:26:29 +02:00
Unrud
06674b224f Update dependencies 2025-10-12 06:26:11 +02:00
Unrud
d48d9aed3d Bump version to 1.5.0 2025-03-18 22:02:15 +01:00
Unrud
5771f44b0e Cleanup portal controller 2025-03-18 09:18:16 +01:00
Unrud
16dc47b570 Encrypt restore token 2025-03-18 09:18:15 +01:00
Unrud
abd9fd4773 Update dependencies 2025-03-17 20:48:45 +01:00
Unrud
7c1652e9dd CI: Install missing package 2025-03-17 20:24:25 +01:00
godalming
0fe41d0b15 Fix not being able to create restore token on snap because the cache directory does not exist 2025-03-17 20:21:35 +01:00
godalming
21461d9a76 Fix file permisions. Detect if restore tokens are not supported (version <= 1). Always log and continue if it is not possible to use a restore token rather than exit the program or log using fmt. 2025-03-17 00:19:48 +01:00
godalming
c6c54c77a7 Add support for session restore on the linux portals backend, fixing #77 2025-03-17 00:19:48 +01:00
Unrud
7a806f7b00 Flatpak: Update runtime 2024-11-17 21:33:51 +01:00
Unrud
4fb452c542 Bump version to 1.4.8 2024-09-01 17:15:32 +02:00
Unrud
c71e5c4b8b Flatpak: Add IPC to fix linting error 2024-09-01 17:12:02 +02:00
Unrud
30cfff86b0 Update go.sum 2024-09-01 17:09:23 +02:00
Unrud
288345b5d3 Bump version to 1.4.7 2024-08-30 23:45:18 +02:00
Unrud
b4ecfad316 Update dependencies 2024-08-30 23:41:45 +02:00
Unrud
dd510bc4a0 Update appdata 2024-04-13 19:03:36 +02:00
Unrud
4916fc59db Update dependencies 2024-04-06 17:22:46 +02:00
Unrud
064c3cd242 appdata: Add vcs-browser URL 2024-04-05 22:38:15 +02:00
Unrud
6996fbf958 appdata: Add developer ID 2024-04-05 22:37:48 +02:00
Unrud
016351e9d2 Bump version to 1.4.6 2024-02-06 11:33:41 +01:00
Unrud
6325782058 Fix CI test 2024-02-06 11:33:40 +01:00
Unrud
0184dcb22f Set MIME types explicitly
Fixes #61
Fixes #70
2024-02-06 11:26:34 +01:00
Unrud
652ee5aa6c Bump version to 1.4.5 2024-01-31 14:59:38 +01:00
Unrud
88fd6e7314 Workaround for virtual keyboard covering buttons for Safari on iOS 2024-01-31 09:33:11 +01:00
Unrud
6a812607fe Fix virtual keyboard covering buttons in Chromium 2024-01-31 09:33:11 +01:00
Unrud
f11cefc74c Keep text input open after sending 2024-01-31 09:33:10 +01:00
Unrud
7cff8286c8 Hide focus outline for textarea 2024-01-31 09:33:10 +01:00
Unrud
280a654f59 Enable hardware keyboard input in mouse mode 2024-01-31 09:33:09 +01:00
Unrud
d502ca651c Disable keyboard navigation for all buttons 2024-01-31 09:33:09 +01:00
Unrud
1985842ff0 Refactor CSS 2024-01-31 09:33:09 +01:00
Unrud
fc363fe404 Snap: Update to core22 2024-01-27 07:40:22 +01:00
Unrud
cc143b59e6 Bump version to 1.4.4 2024-01-23 20:51:57 +01:00
Unrud
02d49bf680 Flatpak: Update dependencies 2024-01-23 20:51:53 +01:00
Unrud
601d8c8358 Update dependencies 2024-01-23 19:42:59 +01:00
Unrud
0ca1431594 Fix Xwayland detection with newer servers
Fixes #63
2024-01-23 19:33:15 +01:00
Unrud
48ff3d67a0 Fix minimum go version
Fixes #62
2024-01-23 14:14:29 +01:00
Unrud
e94fb986c0 Fix typo that causes error with xdg-desktop-portal-kde
Fixes #67
2024-01-23 14:11:16 +01:00
Unrud
7f7604a462 Bump version to 1.4.3 2023-10-04 01:44:59 +02:00
Unrud
e1dbdc9b68 Update dependencies 2023-10-04 01:41:25 +02:00
Unrud
6c746b23f1 Bump version to 1.4.2 2023-05-21 01:01:22 +02:00
Unrud
c8e6664a8d Fix Windows controller
Partially revert 1794150285
Fixes #58
2023-05-21 00:53:33 +02:00
Unrud
1803577892 Bump version to 1.4.1 2023-05-11 23:03:46 +02:00
Unrud
4358c0b0dc Update dependencies 2023-05-11 22:55:04 +02:00
Unrud
737c9671cf Flatpak: Update dependencies 2023-05-09 00:10:03 +02:00
Unrud
925cb745eb Run tests with github actions 2023-05-08 23:58:02 +02:00
Unrud
722678ca4f Update dependencies 2023-05-08 22:49:45 +02:00
Unrud
c6fe56479a uinput: Fix vertical scroll direction 2023-05-08 22:31:34 +02:00
Unrud
1794150285 Simplify error handling 2023-05-08 22:31:02 +02:00
Unrud
6a1a7420dc Update dependencies 2023-05-08 17:48:24 +02:00
Unrud
13f5df3bf8 Bump verstion to 1.4.0 2023-04-25 16:40:10 +02:00
Unrud
bb25f2dd37 Flatpak: Update vendor packages 2023-04-25 16:32:01 +02:00
Unrud
af116f4cfb Explain prevention of mouse mode by touch 2023-04-25 16:31:41 +02:00
Unrud
3c2bec8916 Update dependencies 2023-04-25 01:41:43 +02:00
Unrud
45801d7cd5 Add numeric keypad
Fixes #54
2023-04-25 01:38:52 +02:00
Unrud
4285f6aa1b Add visual and vibration feedback to buttons 2023-04-25 01:38:51 +02:00
Unrud
20cea84056 Prevent touch triggering mouse mode 2023-04-25 01:38:50 +02:00
Unrud
b239ca7160 Use ECMAScript modules 2023-04-25 01:38:50 +02:00
Unrud
e5369af219 Refactor web 2023-04-25 01:38:49 +02:00
Unrud
51108765e3 Add null controller 2023-04-25 01:38:49 +02:00
Unrud
37dff3cb6b uinput: Detect unsupported platform 2023-04-25 01:38:48 +02:00
Unrud
891fde7051 Refactor errors 2023-04-25 01:38:48 +02:00
Unrud
91a37de0fb Bump version to 1.3.0 2023-03-03 23:14:34 +01:00
Unrud
d81b57b1f9 Add uinput build example 2023-03-03 23:06:34 +01:00
Unrud
0bd7260a0c uinput-controller: Show hint for setting keymap 2023-02-27 23:43:10 +01:00
Unrud
e69bd15733 uinput-controller: Lower priority 2023-02-27 23:42:16 +01:00
Unrud
bbb6cc4dc1 uinput-controller: Initial support for keymaps
Keymap can be set with environment variable REMOTE_TOUCHPAD_UINPUT_KEYMAP
2023-02-26 16:45:37 +01:00
Unrud
d254ae5b0d uinput-controller: Map runes/keys directly and handle errors 2023-02-23 02:07:14 +01:00
Unrud
809d3a0064 uinput-controller: Format code 2023-02-23 01:09:29 +01:00
De_Coder
feaf02217c improve uinput controller 2023-02-23 01:09:11 +01:00
De_Coder
f5d19a0354 Add uinput-based controller 2023-02-23 01:09:11 +01:00
Unrud
342334ac18 Update dependencies 2023-02-23 01:09:05 +01:00
Unrud
124152f7ec Include missing rune, key or button in error 2023-02-23 01:09:04 +01:00
Unrud
6f03281fc7 Refactor: Use switch statement 2023-02-23 01:09:03 +01:00
Unrud
79a88dc676 Bump version to 1.2.3 2022-12-29 22:32:47 +01:00
Unrud
9c5dcbee10 Update dependencies 2022-12-29 22:32:47 +01:00
Unrud
f30b4cf48e Flatpak: Add Wayland socket to disable X11 socket 2022-12-29 22:32:38 +01:00
Unrud
681d9c4f1d Improve XWayland detection 2022-12-29 22:19:38 +01:00
Unrud
2fc5d0825b Flatpak: Remove x11 from finish-args 2022-11-03 01:56:15 +01:00
Unrud
c1491f4a84 Snap: Update to core20 2022-09-28 00:48:41 +02:00
Unrud
b96944d4d6 Bump version to 1.2.2 2022-09-24 02:15:04 +02:00
Unrud
61f910adf0 Update dependencies 2022-09-24 02:09:15 +02:00
Unrud
518c6ef952 Fix race in RemoteDesktop portal controller 2022-09-24 02:09:05 +02:00
Unrud
1b3d26c465 Flatpak: Update runtime 2022-09-04 22:13:57 +02:00
Unrud
1675d5d2f8 Bump version to 1.2.1 2022-06-20 23:11:22 +02:00
Unrud
8a2a29c9d6 Reorganize 2022-06-20 03:48:43 +02:00
Unrud
e29982dc00 Update dependencies 2022-06-20 01:19:01 +02:00
Unrud
7522d131ec Update to go 1.17 2022-06-20 01:19:01 +02:00
Unrud
6134fa564b Flatpak: Generate vendor/modules.txt 2022-06-20 01:19:00 +02:00
Unrud
523d2039b6 Bump version to 1.2.0 2022-02-26 13:53:17 +01:00
Unrud
771eea98b5 Increase update rate 2022-02-26 13:53:17 +01:00
Unrud
d6b75771c3 Throw exception again 2022-02-23 21:11:24 +01:00
Unrud
90922d85c6 Return early if no relevant touches 2022-02-23 21:11:23 +01:00
Unrud
0c401e0247 Add missing semicolons and initialize variables 2022-02-22 23:36:56 +01:00
Unrud
b748aceaf9 Prevent soft lockup if an error occurs 2022-02-22 23:36:56 +01:00
Unrud
a9117b82ad Update changelog 2022-02-22 22:40:30 +01:00
Unrud
9543c514d0 Show error message if compiled without controller 2022-02-22 22:40:07 +01:00
Unrud
238ef14843 Rename variable 2022-02-22 22:19:37 +01:00
Unrud
190b2763bd Organize code in namespaces 2022-02-22 22:05:48 +01:00
Unrud
9432869bce Limit update rate 2022-02-22 20:27:39 +01:00
Unrud
61fa13974d README: Switch from "go get" to "go install" 2022-02-22 16:23:00 +01:00
Unrud
e027c71341 Normalize line endings client side 2022-02-22 16:17:55 +01:00
Unrud
a279e195be Windows: Reverse vertical scroll direction 2022-02-21 22:30:29 +01:00
Unrud
25d5cfa505 Rename backend to controller 2022-02-21 22:30:28 +01:00
Unrud
2211b8a662 Remove disabled backends 2022-02-21 22:30:28 +01:00
Unrud
89131db79b Centralized UI updates 2022-02-21 22:30:27 +01:00
Unrud
42dcafda3d Improve variable names 2022-02-21 22:30:26 +01:00
Unrud
9082ca434a Add fullscreenchange event listener 2022-02-21 22:30:26 +01:00
Unrud
98f4d1c692 Add options for configuring pointer and scroll speed 2022-02-21 22:30:25 +01:00
Unrud
fecec13869 Add hardware mouse support 2022-02-21 22:30:20 +01:00
Unrud
1797d7585b Prevent clicks from touches on touchpad 2022-02-21 22:30:20 +01:00
Unrud
db9ca255fa Add Touch prefix to handlers 2022-02-21 22:30:19 +01:00
Unrud
b3db523db1 Keep move and scroll remainder 2022-02-21 22:30:19 +01:00
Unrud
51dbecaa63 Split function updateMoveAndScroll 2022-02-21 22:30:19 +01:00
Unrud
ab09e7c674 Refactor key detection 2022-02-21 22:30:18 +01:00
Unrud
56cff8deb2 Listen for keydown event on document 2022-02-21 22:30:18 +01:00
Unrud
ad3847f5aa Combine scroll and scroll finish commands 2022-02-21 22:30:18 +01:00
Unrud
1f3aa58568 Remove padlabel id 2022-02-21 22:30:17 +01:00
Unrud
41c3e0b10d Switch to go:embed 2022-02-21 22:30:16 +01:00
Unrud
7fc46bf38c Rename class touchpad to touch 2022-02-03 14:51:54 +01:00
Unrud
b3a4ca7449 Use go-bindata instead of go-bindata-assetfs 2022-02-03 14:45:35 +01:00
Unrud
abc8ef5818 Workaround for assetFS FileInfo
See https://github.com/elazarl/go-bindata-assetfs/pull/55
2022-02-01 13:59:32 +01:00
Unrud
c0e6ef52ec Use camel case for variable name 2022-02-01 13:59:32 +01:00
73 changed files with 2869 additions and 1736 deletions

43
.github/workflows/test.yaml vendored Normal file
View 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
View file

@ -0,0 +1,11 @@
# Binaries
remote-touchpad
# Systemd service state
.remote-touchpad.service.state
# Logs and run files
.rts-log
# Environment files
.env

View file

@ -1,5 +1,97 @@
# 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)
* Update dependencies
## 1.2.0 (2022-02-26)
* Add command-line options for move and scroll speed
* Add hardware mouse support
* Limit update rate
* Windows: Reverse vertical scroll direction
* Show error message if compiled without controller
* Don't show disabled controllers in "unsupported platform" error message
* Switch from go-bindata-assetfs to go:embed
## 1.1.0 (2022-01-28) ## 1.1.0 (2022-01-28)
* Add more shortcut keys (Arrows, Delete, Enter, …) * Add more shortcut keys (Arrows, Delete, Enter, …)

View file

@ -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 get -u -tags portal,x11 github.com/Unrud/remote-touchpad` * Portal & uinput & X11:
* Windows: `get get -u github.com/Unrud/remote-touchpad`
```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

View file

@ -1,292 +0,0 @@
// +build portal
/*
* Copyright (c) 2018 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 (
"errors"
"fmt"
"github.com/godbus/dbus/v5"
"sync"
)
const (
deviceKeyboard uint32 = 1
devicePointer uint32 = 2
btnReleased uint32 = 0
btnPressed uint32 = 1
// linux/input-event-codes.h
btnLeft int32 = 0x110
btnRight int32 = 0x111
btnMiddle int32 = 0x112
)
type portalBackend struct {
bus *dbus.Conn
remoteDesktop dbus.BusObject
sessionHandle dbus.ObjectPath
lock sync.RWMutex
}
func InitPortalBackend() (Backend, error) {
bus, err := dbus.SessionBusPrivate()
if err != nil {
return nil, UnsupportedPlatformError{err}
}
cleanupBus := true
defer func() {
if cleanupBus {
bus.Close()
}
}()
err = bus.Auth(nil)
if err != nil {
return nil, UnsupportedPlatformError{err}
}
err = bus.Hello()
if err != nil {
return nil, UnsupportedPlatformError{err}
}
remoteDesktop := bus.Object("org.freedesktop.portal.Desktop",
"/org/freedesktop/portal/desktop")
availableDeviceTypesV, err := remoteDesktop.GetProperty(
"org.freedesktop.portal.RemoteDesktop.AvailableDeviceTypes")
if err != nil {
return nil, UnsupportedPlatformError{err}
}
availableDeviceTypes, ok := availableDeviceTypesV.Value().(uint32)
if !ok {
return nil, UnsupportedPlatformError{errors.New(
"unexpected 'AvailableDeviceTypes' return type")}
}
if availableDeviceTypes&deviceKeyboard == 0 ||
availableDeviceTypes&devicePointer == 0 {
return nil, UnsupportedPlatformError{errors.New(
"keyboard or pointer source type not supported")}
}
inVardict := make(map[string]dbus.Variant)
inVardict["session_handle_token"] = dbus.MakeVariant("t")
result, outVardict, err := getResponse(bus, remoteDesktop,
"org.freedesktop.portal.RemoteDesktop.CreateSession", 0, inVardict)
if err != nil {
return nil, UnsupportedPlatformError{err}
}
if result != 0 {
return nil, UnsupportedPlatformError{errors.New(fmt.Sprintf(
"Calling 'CreateSession' failed (%v)", result))}
}
sessionHandleV, ok := outVardict["session_handle"]
if !ok {
return nil, UnsupportedPlatformError{errors.New(
"'session_handle' missing from '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)
inVardict = make(map[string]dbus.Variant)
inVardict["type"] = dbus.MakeVariant(deviceKeyboard | devicePointer)
result, outVardict, err = getResponse(bus, remoteDesktop,
"org.freedesktop.portal.RemoteDesktop.SelectDevices", 0, sessionHandle, inVardict)
if err != nil {
return nil, UnsupportedPlatformError{err}
}
if result != 0 {
return nil, UnsupportedPlatformError{errors.New(fmt.Sprintf(
"Calling 'SelectDevices' failed (%v)", result))}
}
inVardict = make(map[string]dbus.Variant)
result, outVardict, err = getResponse(bus, remoteDesktop,
"org.freedesktop.portal.RemoteDesktop.Start", 0, sessionHandle, "", inVardict)
if err != nil {
return nil, UnsupportedPlatformError{err}
}
if result != 0 {
return nil, errors.New("keyboard or pointer access denied")
}
devicesV, ok := outVardict["devices"]
if !ok {
return nil, UnsupportedPlatformError{errors.New(
"'devices' missing from 'Start' return value")}
}
devices, ok := devicesV.Value().(uint32)
if !ok {
return nil, UnsupportedPlatformError{errors.New(
"unexpected 'devices' type in 'Start' return value")}
}
if devices&deviceKeyboard == 0 || devices&devicePointer == 0 {
return nil, errors.New("keyboard or pointer access denied")
}
cleanupBus = false
return &portalBackend{bus: bus, remoteDesktop: remoteDesktop,
sessionHandle: sessionHandle}, nil
}
func getResponse(bus *dbus.Conn, object dbus.BusObject, method string,
flags dbus.Flags, args ...interface{}) (uint32, map[string]dbus.Variant, error) {
ch := make(chan *dbus.Signal)
bus.Signal(ch)
defer bus.RemoveSignal(ch)
var requestPath dbus.ObjectPath
if err := object.Call(method, flags, args...).Store(&requestPath); err != nil {
return 0, nil, err
}
for {
s := <-ch
if s.Path == requestPath && s.Name == "org.freedesktop.portal.Request.Response" {
if len(s.Body) != 2 {
return 0, nil, errors.New("unexpected 'Response' return length")
}
result, ok := s.Body[0].(uint32)
if !ok {
return 0, nil, errors.New("unexpected 'Response' return type")
}
outVardict, ok := s.Body[1].(map[string]dbus.Variant)
if !ok {
return 0, nil, errors.New("unexpected 'Response' return type")
}
return result, outVardict, nil
}
}
}
func (p *portalBackend) Close() error {
p.lock.Lock()
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 *portalBackend) 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 _, state := range [...]uint32{btnPressed, btnReleased} {
if err := p.remoteDesktop.Call(
"org.freedesktop.portal.RemoteDesktop.NotifyKeyboardKeysym",
0, p.sessionHandle, inVardict, keysym, state).Store(); err != nil {
return err
}
}
}
return nil
}
func (p *portalBackend) KeyboardText(text string) error {
keys := make([]Keysym, 0, len(text))
for _, runeValue := range text {
keysym, err := RuneToKeysym(runeValue)
if err != nil {
return err
}
keys = append(keys, keysym)
}
return p.keyboardKeys(keys)
}
func (p *portalBackend) KeyboardKey(key Key) error {
keysym, err := KeyToKeysym(key)
if err != nil {
return err
}
keys := [...]Keysym{keysym}
return p.keyboardKeys(keys[:])
}
func (p *portalBackend) 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
if button == PointerButtonLeft {
btn = btnLeft
} else if button == PointerButtonMiddle {
btn = btnMiddle
} else if button == PointerButtonRight {
btn = btnRight
} else {
return errors.New("unsupported pointer button")
}
state := btnReleased
if press {
state = btnPressed
}
inVardict := make(map[string]dbus.Variant)
if err := p.remoteDesktop.Call("org.freedesktop.portal.RemoteDesktop.NotifyPointerButton",
0, p.sessionHandle, inVardict, btn, state).Store(); err != nil {
return err
}
return nil
}
func (p *portalBackend) PointerMove(deltaX, deltaY int) error {
p.lock.RLock()
defer p.lock.RUnlock()
if p.bus == 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 nil
}
func (p *portalBackend) pointerScrollFull(deltaHorizontal, deltaVertical int, finish bool) error {
p.lock.RLock()
defer p.lock.RUnlock()
if p.bus == 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 nil
}
func (p *portalBackend) PointerScroll(deltaHorizontal, deltaVertical int) error {
return p.pointerScrollFull(deltaHorizontal, deltaVertical, false)
}
func (p *portalBackend) PointerScrollFinish() error {
return p.pointerScrollFull(0, 0, true)
}

View file

@ -1,28 +0,0 @@
// +build !x11
/*
* Copyright (c) 2018 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 "errors"
func InitX11Backend() (Backend, error) {
return nil, UnsupportedPlatformError{errors.New("disabled")}
}

File diff suppressed because one or more lines are too long

View file

@ -1,4 +0,0 @@
package main
//go:generate go-bindata-assetfs -mode 0664 -modtime 1 -o ./bindata.generated.go webdata/...
//go:generate gofmt -w ./bindata.generated.go

View file

@ -17,8 +17,159 @@
<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 &amp; 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">
<description>
<ul>
<li>Update dependencies</li>
</ul>
</description>
</release>
<release version="1.2.0" date="2022-02-26">
<description>
<ul>
<li>Add command-line options for move and scroll speed</li>
<li>Add hardware mouse support</li>
<li>Limit update rate</li>
<li>Windows: Reverse vertical scroll direction</li>
<li>Show error message if compiled without controller</li>
<li>Don't show disabled controllers in "unsupported platform" error message</li>
<li>Switch from go-bindata-assetfs to go:embed</li>
</ul>
</description>
</release>
<release version="1.1.0" date="2022-01-28"> <release version="1.1.0" date="2022-01-28">
<description> <description>
<ul> <ul>
@ -222,16 +373,16 @@
</releases> </releases>
<screenshots> <screenshots>
<screenshot type="default"> <screenshot type="default">
<image>https://raw.githubusercontent.com/Unrud/remote-touchpad/v1.1.0/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.1.0/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.1.0/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.1.0/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"/>

View file

@ -1,27 +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:
# Vendor dependencies: go mod vendor -v
- type: git - type: git
url: https://github.com/elazarl/go-bindata-assetfs path: ..
tag: v1.0.1
commit: 234c15e7648ff35458026de92b34c637bae5e6f7
dest: vendor/github.com/elazarl/go-bindata-assetfs
- type: git - type: git
url: https://github.com/godbus/dbus url: https://github.com/godbus/dbus
tag: v5.0.6 tag: v5.2.2
commit: daa017464e266380cdeb4e6f9613eba9182b59a3 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
@ -29,14 +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: cd36cc0744dd695657988f15f08446dc81e16efc 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 - type: file
path: modules.txt path: modules.txt
dest: vendor dest: vendor
- type: git
path: ..
branch: master
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

View file

@ -1,14 +1,17 @@
# github.com/elazarl/go-bindata-assetfs v1.0.1 # github.com/bendahl/uinput v1.7.0
## explicit ## explicit; go 1.13
github.com/elazarl/go-bindata-assetfs github.com/bendahl/uinput
# github.com/godbus/dbus/v5 v5.0.6 # github.com/godbus/dbus/v5 v5.2.2
## explicit ## explicit; go 1.20
github.com/godbus/dbus/v5 github.com/godbus/dbus/v5
# github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e # github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
## explicit ## explicit; go 1.13
github.com/skip2/go-qrcode github.com/skip2/go-qrcode
github.com/skip2/go-qrcode/bitset github.com/skip2/go-qrcode/bitset
github.com/skip2/go-qrcode/reedsolomon github.com/skip2/go-qrcode/reedsolomon
# golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd # golang.org/x/net v0.57.0
## explicit ## explicit; go 1.25.0
golang.org/x/net/websocket golang.org/x/net/websocket
# golang.org/x/sys v0.47.0
## explicit; go 1.25.0
golang.org/x/sys/unix

10
go.mod
View file

@ -1,10 +1,12 @@
module github.com/unrud/remote-touchpad module github.com/unrud/remote-touchpad
go 1.15 go 1.25.5
require ( require (
github.com/elazarl/go-bindata-assetfs v1.0.1 github.com/bendahl/uinput v1.7.0
github.com/godbus/dbus/v5 v5.0.6 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-20220127200216-cd36cc0744dd golang.org/x/net v0.57.0
) )
require golang.org/x/sys v0.47.0 // indirect

19
go.sum
View file

@ -1,13 +1,10 @@
github.com/elazarl/go-bindata-assetfs v1.0.1 h1:m0kkaHRKEu7tUIUFVwhGGGYClXvyl4RE03qmvRTNfbw= github.com/bendahl/uinput v1.7.0 h1:nA4fm8Wu8UYNOPykIZm66nkWEyvxzfmJ8YC02PM40jg=
github.com/elazarl/go-bindata-assetfs v1.0.1/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4= github.com/bendahl/uinput v1.7.0/go.mod h1:Np7w3DINc9wB83p12fTAM3DPPhFnAKP0WTXRqCQJ6Z8=
github.com/godbus/dbus/v5 v5.0.6 h1:mkgN1ofwASrYnJ5W6U/BxG15eXXXjirgZc7CLqkcaro= github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 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-20220127200216-cd36cc0744dd h1:O7DYs+zxREGLKzKoMQrtrEacpb0ZVXA5rIwylE2Xchk= golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 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-20211216021012-1d35b9e2eb4e/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=

View file

@ -28,7 +28,7 @@ import (
const ipv4Rating int = +1 const ipv4Rating int = +1
func FindDefaultHost() string { func findDefaultHost() string {
type hostsValue struct { type hostsValue struct {
prio int prio int
host string host string
@ -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

View file

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

Before After
Before After

View file

@ -17,10 +17,14 @@
* along with Remote-Touchpad. If not, see <http://www.gnu.org/licenses/>. * along with Remote-Touchpad. If not, see <http://www.gnu.org/licenses/>.
*/ */
package main package inputcontrol
type PointerButton int import "sort"
type Key int
type (
PointerButton int
Key int
)
const ( const (
PointerButtonLeft PointerButton = iota PointerButtonLeft PointerButton = iota
@ -51,31 +55,39 @@ const (
KeyLimit KeyLimit
) )
type BackendInfo struct { type ControllerInfo struct {
Name string Name string
Init func() (Backend, error) Init func() (Controller, error)
priority int
} }
var Backends []BackendInfo = []BackendInfo{ var Controllers []ControllerInfo
{"X11", InitX11Backend},
{"RemoteDesktop portal", InitPortalBackend}, func RegisterController(name string, init func() (Controller, error), priority int) {
{"Windows", InitWindowsBackend}, Controllers = append(Controllers, ControllerInfo{name, init, priority})
sort.SliceStable(Controllers, func(i, j int) bool {
return Controllers[i].priority < Controllers[j].priority
})
} }
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()
} }
type Backend interface { func (e *UnsupportedPlatformError) Unwrap() error {
return e.Err
}
type Controller interface {
Close() error Close() error
KeyboardText(text string) error KeyboardText(text string) error
KeyboardKey(key Key) error KeyboardKey(key Key) error
PointerButton(button PointerButton, press bool) error PointerButton(button PointerButton, press bool) error
PointerMove(deltaX, deltaY int) error PointerMove(deltaX, deltaY int) error
PointerScroll(deltaHorizontal, deltaVertical int) error PointerScroll(deltaHorizontal, deltaVertical int, finish bool) error
PointerScrollFinish() error
} }

View 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
}

View file

@ -0,0 +1,400 @@
//go:build portal
/*
* Copyright (c) 2018 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 (
"crypto/aes"
"crypto/cipher"
"crypto/hkdf"
"crypto/rand"
"crypto/sha256"
"errors"
"fmt"
"io"
"log"
"os"
"path/filepath"
"slices"
"github.com/godbus/dbus/v5"
)
const (
deviceKeyboard uint32 = 1
devicePointer uint32 = 2
btnReleased uint32 = 0
btnPressed uint32 = 1
untilRevoked uint32 = 2
// linux/input-event-codes.h
btnLeft int32 = 0x110
btnRight int32 = 0x111
btnMiddle int32 = 0x112
)
type portalController struct {
bus *dbus.Conn
portalDesktop dbus.BusObject
sessionHandle dbus.ObjectPath
}
func init() {
RegisterController("RemoteDesktop portal", InitPortalController, 1)
}
func InitPortalController() (Controller, error) {
bus, err := dbus.SessionBusPrivate()
if err != nil {
return nil, &UnsupportedPlatformError{err}
}
cleanupBus := true
defer func() {
if cleanupBus {
bus.Close()
}
}()
err = bus.Auth(nil)
if err != nil {
return nil, &UnsupportedPlatformError{err}
}
err = bus.Hello()
if err != nil {
return nil, &UnsupportedPlatformError{err}
}
portalDesktop := bus.Object("org.freedesktop.portal.Desktop",
"/org/freedesktop/portal/desktop")
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")
if err != nil {
return nil, &UnsupportedPlatformError{
fmt.Errorf("getting 'AvailableDeviceTypes' failed: %w", err),
}
}
availableDeviceTypes, ok := availableDeviceTypesV.Value().(uint32)
if !ok {
return nil, &UnsupportedPlatformError{
errors.New("unexpected 'AvailableDeviceTypes' return type"),
}
}
if availableDeviceTypes&deviceKeyboard == 0 ||
availableDeviceTypes&devicePointer == 0 {
return nil, &UnsupportedPlatformError{
errors.New("keyboard or pointer source type not supported"),
}
}
createSessionResults, err := checkResponse(getResponse(bus, portalDesktop,
"org.freedesktop.portal.RemoteDesktop.CreateSession", 0,
map[string]dbus.Variant{"session_handle_token": dbus.MakeVariant("t")},
))
if err != nil {
return nil, &UnsupportedPlatformError{
fmt.Errorf("calling 'CreateSession' failed: %w", err),
}
}
sessionHandleString, ok := createSessionResults["session_handle"].Value().(string)
if !ok {
return nil, &UnsupportedPlatformError{
errors.New("unexpected 'session_handle' type in 'CreateSession' return value"),
}
}
sessionHandle := dbus.ObjectPath(sessionHandleString)
selectDevicesOptions := map[string]dbus.Variant{
"types": dbus.MakeVariant(deviceKeyboard | devicePointer),
}
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 {
return nil, &UnsupportedPlatformError{
fmt.Errorf("calling 'SelectDevices' failed: %w", err),
}
}
startResponseStatus, startResults, err := getResponse(bus, portalDesktop,
"org.freedesktop.portal.RemoteDesktop.Start", 0,
sessionHandle, "", map[string]dbus.Variant{},
)
if err != nil {
return nil, &UnsupportedPlatformError{
fmt.Errorf("calling 'Start' failed: %w", err),
}
}
if startResponseStatus != 0 {
return nil, errors.New("keyboard or pointer access denied")
}
if restoreToken, _ := startResults["restore_token"].Value().(string); restoreTokenStore != nil {
if err := restoreTokenStore.Store([]byte(restoreToken)); err != nil {
log.Printf("Failed to store restore token: %v", err)
}
}
devices, ok := startResults["devices"].Value().(uint32)
if !ok {
return nil, &UnsupportedPlatformError{
errors.New("unexpected 'devices' type in 'Start' return value"),
}
}
if devices&deviceKeyboard == 0 || devices&devicePointer == 0 {
return nil, errors.New("keyboard or pointer access denied")
}
cleanupBus = false
return &portalController{
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,
flags dbus.Flags, args ...interface{},
) (uint32, map[string]dbus.Variant, error) {
ch := make(chan *dbus.Signal, 512)
bus.Signal(ch)
defer bus.RemoveSignal(ch)
var requestPath dbus.ObjectPath
if err := object.Call(method, flags, args...).Store(&requestPath); err != nil {
return 0, nil, err
}
for {
s := <-ch
if s.Path == requestPath && s.Name == "org.freedesktop.portal.Request.Response" {
if len(s.Body) != 2 {
return 0, nil, fmt.Errorf("unexpected 'Response' return length (%d)", len(s.Body))
}
responseStatus, ok := s.Body[0].(uint32)
if !ok {
return 0, nil, errors.New("unexpected 'Response' return type")
}
results, ok := s.Body[1].(map[string]dbus.Variant)
if !ok {
return 0, nil, errors.New("unexpected 'Response' return type")
}
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 {
return p.bus.Close()
}
func (p *portalController) keyboardKeys(keys []Keysym) error {
for _, keysym := range keys {
for _, state := range [...]uint32{btnPressed, btnReleased} {
if err := p.portalDesktop.Call(
"org.freedesktop.portal.RemoteDesktop.NotifyKeyboardKeysym", 0,
p.sessionHandle, map[string]dbus.Variant{}, keysym, state,
).Store(); err != nil {
return err
}
}
}
return nil
}
func (p *portalController) KeyboardText(text string) error {
keys := make([]Keysym, 0, len(text))
for _, runeValue := range text {
keysym, err := RuneToKeysym(runeValue)
if err != nil {
return err
}
keys = append(keys, keysym)
}
return p.keyboardKeys(keys)
}
func (p *portalController) KeyboardKey(key Key) error {
keysym, err := KeyToKeysym(key)
if err != nil {
return err
}
keys := [...]Keysym{keysym}
return p.keyboardKeys(keys[:])
}
func (p *portalController) PointerButton(button PointerButton, press bool) error {
var btn int32
switch button {
case PointerButtonLeft:
btn = btnLeft
case PointerButtonMiddle:
btn = btnMiddle
case PointerButtonRight:
btn = btnRight
default:
return fmt.Errorf("unsupported pointer button: %#v", button)
}
state := btnReleased
if press {
state = btnPressed
}
if err := p.portalDesktop.Call("org.freedesktop.portal.RemoteDesktop.NotifyPointerButton", 0,
p.sessionHandle, map[string]dbus.Variant{}, btn, state,
).Store(); err != nil {
return err
}
return nil
}
func (p *portalController) PointerMove(deltaX, deltaY int) error {
if err := p.portalDesktop.Call("org.freedesktop.portal.RemoteDesktop.NotifyPointerMotion", 0,
p.sessionHandle, map[string]dbus.Variant{}, float64(deltaX), float64(deltaY),
).Store(); err != nil {
return err
}
return nil
}
func (p *portalController) PointerScroll(deltaHorizontal, deltaVertical int, finish bool) error {
if err := p.portalDesktop.Call("org.freedesktop.portal.RemoteDesktop.NotifyPointerAxis", 0,
p.sessionHandle, map[string]dbus.Variant{"finish": dbus.MakeVariant(finish)}, float64(deltaHorizontal), float64(deltaVertical),
).Store(); err != nil {
return err
}
return nil
}

View 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)))
}

View file

@ -1,4 +1,4 @@
// +build windows //go:build windows
/* /*
* Copyright (c) 2018 Unrud <unrud@outlook.com> * Copyright (c) 2018 Unrud <unrud@outlook.com>
@ -19,10 +19,10 @@
* along with Remote-Touchpad. If not, see <http://www.gnu.org/licenses/>. * along with Remote-Touchpad. If not, see <http://www.gnu.org/licenses/>.
*/ */
package main package inputcontrol
import ( import (
"errors" "fmt"
"syscall" "syscall"
"unsafe" "unsafe"
) )
@ -89,33 +89,36 @@ type keybdInput struct {
padding [8]byte padding [8]byte
} }
type windowsBackend struct{} type windowsController struct{}
func InitWindowsBackend() (Backend, error) { func init() {
p := &windowsBackend{} RegisterController("Windows", InitWindowsController, 0)
}
func InitWindowsController() (Controller, error) {
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
} }
func (p *windowsBackend) Close() error { func (p *windowsController) Close() error {
return nil return nil
} }
func (p *windowsBackend) sendInput(inputs []keybdInput) error { 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
} }
func (p *windowsBackend) KeyboardText(text string) error { func (p *windowsController) KeyboardText(text string) error {
inputs := make([]keybdInput, 0, len(text)*2) inputs := make([]keybdInput, 0, len(text)*2)
for _, runeValue := range text { for _, runeValue := range text {
in := keybdInput{typ: inputKeyboard, wScan: uint16(runeValue), dwFlags: keyeventfUnicode} in := keybdInput{typ: inputKeyboard, wScan: uint16(runeValue), dwFlags: keyeventfUnicode}
@ -129,53 +132,54 @@ func (p *windowsBackend) KeyboardText(text string) error {
return p.sendInput(inputs) return p.sendInput(inputs)
} }
func (p *windowsBackend) 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
return p.sendInput(inputs[:]) return p.sendInput(inputs[:])
} }
func (p *windowsBackend) PointerButton(button PointerButton, press bool) error { func (p *windowsController) PointerButton(button PointerButton, press bool) error {
input := mouseInput{typ: inputMouse} input := mouseInput{typ: inputMouse}
if button == PointerButtonLeft && press { if button == PointerButtonLeft && press {
input.dwFlags = mouseeventfLeftdown input.dwFlags = mouseeventfLeftdown
@ -190,30 +194,30 @@ func (p *windowsBackend) PointerButton(button PointerButton, press bool) error {
} 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
} }
func (p *windowsBackend) PointerMove(deltaX, deltaY int) error { func (p *windowsController) PointerMove(deltaX, deltaY int) error {
input := mouseInput{ input := mouseInput{
typ: inputMouse, typ: inputMouse,
dx: int32(deltaX), dx: int32(deltaX),
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
} }
func (p *windowsBackend) PointerScroll(deltaHorizontal, deltaVertical int) error { func (p *windowsController) PointerScroll(deltaHorizontal, deltaVertical int, finish bool) error {
inputs := make([]mouseInput, 0, 2) inputs := make([]mouseInput, 0, 2)
if deltaHorizontal != 0 { if deltaHorizontal != 0 {
inputs = append(inputs, mouseInput{ inputs = append(inputs, mouseInput{
@ -226,20 +230,15 @@ func (p *windowsBackend) PointerScroll(deltaHorizontal, deltaVertical int) error
inputs = append(inputs, mouseInput{ inputs = append(inputs, mouseInput{
typ: inputMouse, typ: inputMouse,
dwFlags: mouseeventfWheel, dwFlags: mouseeventfWheel,
mouseData: uint32(deltaVertical * scrollMult), mouseData: uint32(-deltaVertical * scrollMult),
}) })
} }
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
} }
func (p *windowsBackend) PointerScrollFinish() error {
return nil
}

View file

@ -1,4 +1,4 @@
// +build x11 //go:build x11
/* /*
* Copyright (c) 2018 Unrud <unrud@outlook.com> * Copyright (c) 2018 Unrud <unrud@outlook.com>
@ -19,52 +19,101 @@
* along with Remote-Touchpad. If not, see <http://www.gnu.org/licenses/>. * along with Remote-Touchpad. If not, see <http://www.gnu.org/licenses/>.
*/ */
package main 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 x11Backend struct { type x11Controller struct {
display *C.Display display *C.Display
lock sync.Mutex lock sync.Mutex
scrollHorizontal, scrollVertical int scrollHorizontal, scrollVertical int
} }
func InitX11Backend() (Backend, error) { func init() {
sessionType := os.Getenv("XDG_SESSION_TYPE") RegisterController("X11", InitX11Controller, 0)
if sessionType != "" && sessionType != "x11" {
return nil, UnsupportedPlatformError{errors.New(fmt.Sprintf(
"unsupported session type '%v'", sessionType))}
}
display := C.XOpenDisplay(nil)
if display == nil {
return nil, UnsupportedPlatformError{
errors.New("failed to connect to X server")}
}
return &x11Backend{display: display}, nil
} }
func (p *x11Backend) Close() error { func InitX11Controller() (Controller, error) {
display := C.XOpenDisplay(nil)
if display == nil {
return nil, &UnsupportedPlatformError{
errors.New("failed to connect to X server"),
}
}
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 {
p.lock.Lock() p.lock.Lock()
defer p.lock.Unlock() defer p.lock.Unlock()
if p.display == nil { if p.display == nil {
@ -75,53 +124,68 @@ func (p *x11Backend) Close() error {
return nil return nil
} }
func (p *x11Backend) 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 *x11Backend) 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 *x11Backend) 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
} }
} }
@ -129,56 +193,45 @@ func (p *x11Backend) getModKeycodesLocked() map[uint]C.KeyCode {
return modKeycodes return modKeycodes
} }
func (p *x11Backend) 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 retKeysym C.KeySym
C.XkbTranslateKeyCode(keyboard, keycode, mods, &retMods, &retKeysym)
if retKeysym == C.KeySym(keysym) {
return keycode, mods
} }
} }
var retMods C.uint
var retKeysym C.KeySym
C.XkbTranslateKeyCode(keyboard, keycode, C.uint(mods), &retMods, &retKeysym)
if retKeysym == C.KeySym(keysym) {
return keycode, mods
}
} }
return 0, 0 return 0, 0
} }
func (p *x11Backend) 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)
} }
} }
} }
func (p *x11Backend) keyboardKeys(keys []Keysym) error { func (p *x11Controller) keyboardKeys(keys []Keysym) error {
p.lock.Lock() p.lock.Lock()
defer p.lock.Unlock() defer p.lock.Unlock()
if p.display == nil { if p.display == nil {
@ -193,17 +246,22 @@ func (p *x11Backend) 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
@ -211,12 +269,10 @@ func (p *x11Backend) 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
@ -227,7 +283,7 @@ func (p *x11Backend) 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)
@ -236,7 +292,7 @@ func (p *x11Backend) keyboardKeys(keys []Keysym) error {
return nil return nil
} }
func (p *x11Backend) KeyboardText(text string) error { func (p *x11Controller) KeyboardText(text string) error {
keys := make([]Keysym, 0, len(text)) keys := make([]Keysym, 0, len(text))
for _, runeValue := range text { for _, runeValue := range text {
keysym, err := RuneToKeysym(runeValue) keysym, err := RuneToKeysym(runeValue)
@ -248,7 +304,7 @@ func (p *x11Backend) KeyboardText(text string) error {
return p.keyboardKeys(keys) return p.keyboardKeys(keys)
} }
func (p *x11Backend) KeyboardKey(key Key) error { func (p *x11Controller) KeyboardKey(key Key) error {
keysym, err := KeyToKeysym(key) keysym, err := KeyToKeysym(key)
if err != nil { if err != nil {
return err return err
@ -257,7 +313,7 @@ func (p *x11Backend) KeyboardKey(key Key) error {
return p.keyboardKeys(keys[:]) return p.keyboardKeys(keys[:])
} }
func (p *x11Backend) sendButton(button uint, press bool) error { func (p *x11Controller) sendButton(button uint, press bool) error {
p.lock.Lock() p.lock.Lock()
defer p.lock.Unlock() defer p.lock.Unlock()
if p.display == nil { if p.display == nil {
@ -271,47 +327,52 @@ func (p *x11Backend) 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 *x11Backend) 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 *x11Backend) PointerMove(deltaX, deltaY int) error { func (p *x11Controller) PointerMove(deltaX, deltaY int) error {
p.lock.Lock() p.lock.Lock()
defer p.lock.Unlock() defer p.lock.Unlock()
if p.display == nil { if p.display == nil {
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
} }
func (p *x11Backend) PointerScroll(deltaHorizontal, deltaVertical int) error { func (p *x11Controller) PointerScroll(deltaHorizontal, deltaVertical int, finish bool) error {
p.lock.Lock() p.lock.Lock()
stepsHorizontal := (p.scrollHorizontal + deltaHorizontal) / scrollDiv stepsHorizontal := (p.scrollHorizontal + deltaHorizontal) / scrollDiv
stepsVertical := (p.scrollVertical + deltaVertical) / scrollDiv stepsVertical := (p.scrollVertical + deltaVertical) / scrollDiv
p.scrollHorizontal = (p.scrollHorizontal + deltaHorizontal) % scrollDiv if finish {
p.scrollVertical = (p.scrollVertical + deltaVertical) % scrollDiv p.scrollHorizontal = 0
p.scrollVertical = 0
} else {
p.scrollHorizontal = (p.scrollHorizontal + deltaHorizontal) % scrollDiv
p.scrollVertical = (p.scrollVertical + deltaVertical) % scrollDiv
}
p.lock.Unlock() p.lock.Unlock()
var buttonHorizontal uint = 7 var buttonHorizontal uint = 7
if stepsHorizontal < 0 { if stepsHorizontal < 0 {
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
} }
@ -324,7 +385,7 @@ func (p *x11Backend) PointerScroll(deltaHorizontal, deltaVertical int) error {
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
} }
@ -334,11 +395,3 @@ func (p *x11Backend) PointerScroll(deltaHorizontal, deltaVertical int) error {
} }
return nil return nil
} }
func (p *x11Backend) PointerScrollFinish() error {
p.lock.Lock()
defer p.lock.Unlock()
p.scrollHorizontal = 0
p.scrollVertical = 0
return nil
}

130
inputcontrol/keymap.go Normal file
View 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
}

View file

@ -1,7 +1,7 @@
// Code generated by keysyms.generator.go. DO NOT EDIT. // Code generated by keysyms.generator.go. DO NOT EDIT.
// +build portal x11 //go:build portal || x11
package main package inputcontrol
var keysymsMap = map[rune]Keysym{ var keysymsMap = map[rune]Keysym{
0x0008: 0x0000ff08, 0x0008: 0x0000ff08,

View file

@ -1,4 +1,4 @@
// +build ignore //go:build ignore
/* /*
* Copyright (c) 2018 Unrud <unrud@outlook.com> * Copyright (c) 2018 Unrud <unrud@outlook.com>
@ -102,8 +102,8 @@ func main() {
keysymsMap[unicode] = keysym keysymsMap[unicode] = keysym
} }
content := "// Code generated by keysyms.generator.go. DO NOT EDIT.\n" + content := "// Code generated by keysyms.generator.go. DO NOT EDIT.\n" +
"// +build portal x11\n\n" + "//go:build portal || x11\n\n" +
"package main\n\n" + "package inputcontrol\n\n" +
"var keysymsMap = map[rune]Keysym{\n" "var keysymsMap = map[rune]Keysym{\n"
keys := make([]rune, 0) keys := make([]rune, 0)
for key := range keysymsMap { for key := range keysymsMap {

View file

@ -1,4 +1,4 @@
// +build portal x11 //go:build portal || x11
/* /*
* Copyright (c) 2018 Unrud <unrud@outlook.com> * Copyright (c) 2018 Unrud <unrud@outlook.com>
@ -19,11 +19,11 @@
* along with Remote-Touchpad. If not, see <http://www.gnu.org/licenses/>. * along with Remote-Touchpad. If not, see <http://www.gnu.org/licenses/>.
*/ */
package main 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")
} }

93
main.go
View file

@ -27,7 +27,6 @@ import (
"errors" "errors"
"flag" "flag"
"fmt" "fmt"
"golang.org/x/net/websocket"
"log" "log"
mathrand "math/rand" mathrand "math/rand"
"net" "net"
@ -37,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 (
@ -45,26 +48,31 @@ const (
authenticationRateBurst int = 10 authenticationRateBurst int = 10
challengeLength int = 8 challengeLength int = 8
defaultBind string = ":0" defaultBind string = ":0"
version string = "1.1.0" version string = "1.5.4"
pretty_app_name string = "Remote Touchpad" prettyAppName string = "Remote Touchpad"
) )
func processCommand(backend Backend, command string) error { type config struct {
UpdateRate uint `json:"updateRate"`
ScrollSpeed float64 `json:"scrollSpeed"`
MoveSpeed float64 `json:"moveSpeed"`
MouseScrollSpeed float64 `json:"mouseScrollSpeed"`
MouseMoveSpeed float64 `json:"mouseMoveSpeed"`
}
func processCommand(controller inputcontrol.Controller, command string) error {
if len(command) == 0 { if len(command) == 0 {
return errors.New("empty command") return errors.New("empty command")
} }
if command == "sf" { if command == "S" {
return backend.PointerScrollFinish() return controller.PointerScroll(0, 0, true)
} }
if command[0] == 't' { if command[0] == 't' {
text := command[1:] text := command[1:]
// normalize line endings
text = strings.Replace(text, "\r\n", "\n", -1)
text = strings.Replace(text, "\r", "\n", -1)
if !utf8.ValidString(text) { if !utf8.ValidString(text) {
return errors.New("invalid utf-8") return errors.New("invalid utf-8")
} }
return backend.KeyboardText(text) return controller.KeyboardText(text)
} }
arguments := strings.Split(command[1:], ";") arguments := strings.Split(command[1:], ";")
if command[0] == 'k' && len(arguments) != 1 || if command[0] == 'k' && len(arguments) != 1 ||
@ -76,30 +84,33 @@ func processCommand(backend Backend, command string) error {
return err return err
} }
if command[0] == 'k' { if command[0] == 'k' {
if x < 0 || x >= int64(KeyLimit) { if x < 0 || x >= int64(inputcontrol.KeyLimit) {
return errors.New("unsupported key") return errors.New("unsupported key")
} }
return backend.KeyboardKey(Key(x)) return controller.KeyboardKey(inputcontrol.Key(x))
} }
y, err := strconv.ParseInt(arguments[1], 10, 32) y, err := strconv.ParseInt(arguments[1], 10, 32)
if err != nil { if err != nil {
return err return err
} }
if command[0] == 'm' { if command[0] == 'm' {
return backend.PointerMove(int(x), int(y)) return controller.PointerMove(int(x), int(y))
} }
if command[0] == 's' { if command[0] == 's' {
return backend.PointerScroll(int(x), int(y)) return controller.PointerScroll(int(x), int(y), false)
}
if command[0] == 'S' {
return controller.PointerScroll(int(x), int(y), true)
} }
if command[0] == 'b' { if command[0] == 'b' {
if x < 0 || x >= int64(PointerButtonLimit) { if x < 0 || x >= int64(inputcontrol.PointerButtonLimit) {
return errors.New("unsupported pointer button") return errors.New("unsupported pointer button")
} }
b := true b := true
if y == 0 { if y == 0 {
b = false b = false
} }
return backend.PointerButton(PointerButton(x), b) return controller.PointerButton(inputcontrol.PointerButton(x), b)
} }
return errors.New("unsupported command") return errors.New("unsupported command")
} }
@ -140,14 +151,20 @@ func secureRandBase64(length int) string {
} }
func main() { func main() {
TerminalSetTitle(pretty_app_name) terminal.SetTitle(prettyAppName)
var bind, certFile, keyFile, secret string var bind, certFile, keyFile, secret string
var showVersion bool var showVersion bool
var config config
flag.BoolVar(&showVersion, "version", false, "show program's version number and exit") flag.BoolVar(&showVersion, "version", false, "show program's version number and exit")
flag.StringVar(&bind, "bind", defaultBind, "bind server to [HOSTNAME]:PORT") flag.StringVar(&bind, "bind", defaultBind, "bind server to [HOSTNAME]:PORT")
flag.StringVar(&secret, "secret", "", "shared secret for client authentication") flag.StringVar(&secret, "secret", "", "shared secret for client authentication")
flag.StringVar(&certFile, "cert", "", "file containing TLS certificate") flag.StringVar(&certFile, "cert", "", "file containing TLS certificate")
flag.StringVar(&keyFile, "key", "", "file containing TLS private key") flag.StringVar(&keyFile, "key", "", "file containing TLS private key")
flag.UintVar(&config.UpdateRate, "update-rate", 30, "number of updates per second")
flag.Float64Var(&config.MoveSpeed, "move-speed", 1, "move speed multiplier")
flag.Float64Var(&config.ScrollSpeed, "scroll-speed", 1, "scroll speed multiplier")
flag.Float64Var(&config.MouseMoveSpeed, "mouse-move-speed", 1, "mouse move speed multiplier")
flag.Float64Var(&config.MouseScrollSpeed, "mouse-scroll-speed", 1, "mouse scroll speed multiplier")
flag.Parse() flag.Parse()
if showVersion { if showVersion {
fmt.Println(version) fmt.Println(version)
@ -163,25 +180,32 @@ func main() {
if secret == "" { if secret == "" {
secret = secureRandBase64(defaultSecretLength) secret = secureRandBase64(defaultSecretLength)
} }
var backend Backend if len(inputcontrol.Controllers) == 0 {
var backendName string log.Fatal("compiled without controller")
platformErrors := "" }
for _, backendInfo := range Backends { var controller inputcontrol.Controller
backendName = backendInfo.Name var controllerName string
var platformErrs []error
for _, controllerInfo := range inputcontrol.Controllers {
controllerName = controllerInfo.Name
var err error var err error
backend, err = backendInfo.Init() controller, err = controllerInfo.Init()
if err == nil { if err == nil {
break break
} else if _, ok := err.(UnsupportedPlatformError); ok {
platformErrors += fmt.Sprintf("%s backend: %v\n", backendName, err)
} else { } else {
log.Fatalf("%s backend: %v", backendName, 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 backend == nil { if controller == nil {
log.Fatal("unsupported platform:\n" + platformErrors) log.Fatal(fmt.Errorf("unsupported platform:\n%w", errors.Join(platformErrs...)))
} }
defer backend.Close() defer controller.Close()
authenticationChallenges := make(chan challenge, authenticationRateBurst) authenticationChallenges := make(chan challenge, authenticationRateBurst)
go authenticationChallengeGenerator(secret, authenticationChallenges) go authenticationChallengeGenerator(secret, authenticationChallenges)
listener, err := net.Listen("tcp", bind) listener, err := net.Listen("tcp", bind)
@ -201,11 +225,11 @@ func main() {
} }
} }
if host == "" { if host == "" {
host = FindDefaultHost() host = findDefaultHost()
} }
port := addr.Port port := addr.Port
mux := http.NewServeMux() mux := http.NewServeMux()
mux.Handle("/", http.FileServer(assetFS())) mux.Handle("/", http.FileServer(http.FS(webdataFS)))
mux.Handle("/ws", websocket.Handler(func(ws *websocket.Conn) { mux.Handle("/ws", websocket.Handler(func(ws *websocket.Conn) {
var message string var message string
challenge := <-authenticationChallenges challenge := <-authenticationChallenges
@ -216,12 +240,13 @@ func main() {
if !challenge.verify(message) { if !challenge.verify(message) {
return return
} }
websocket.JSON.Send(ws, config)
for { for {
if err := websocket.Message.Receive(ws, &message); err != nil { if err := websocket.Message.Receive(ws, &message); err != nil {
return return
} }
if err := processCommand(backend, message); err != nil { if err := processCommand(controller, message); err != nil {
log.Printf("%s backend: %v", backendName, err) log.Print(fmt.Errorf("%s controller: %w", controllerName, err))
return return
} }
} }
@ -236,7 +261,7 @@ func main() {
} }
url := fmt.Sprintf("%s://%s/#%s", scheme, domain, secret) url := fmt.Sprintf("%s://%s/#%s", scheme, domain, secret)
fmt.Println(url) fmt.Println(url)
if qrCode, err := GenerateQRCode(url, TerminalSupportsColor(os.Stdout.Fd())); err == nil { if qrCode, err := terminal.GenerateQRCode(url, terminal.SupportsColor(os.Stdout.Fd())); err == nil {
fmt.Print(qrCode) fmt.Print(qrCode)
} else { } else {
log.Printf("QR code error: %v", err) log.Printf("QR code error: %v", err)

12
remote-touchpad.service Normal file
View 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
View 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

View file

@ -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]

View file

@ -17,7 +17,7 @@
* along with Remote-Touchpad. If not, see <http://www.gnu.org/licenses/>. * along with Remote-Touchpad. If not, see <http://www.gnu.org/licenses/>.
*/ */
package main package terminal
import qrcode "github.com/skip2/go-qrcode" import qrcode "github.com/skip2/go-qrcode"
@ -33,7 +33,7 @@ func qrCodeToString(bits [][]bool, colorize bool) string {
s := "" s := ""
for y := -1; y < len(bits); y += 2 { for y := -1; y < len(bits); y += 2 {
if colorize { if colorize {
s += TerminalForegroundBlack + TerminalBackgroundWhite s += ForegroundBlack + BackgroundWhite
} }
for x := range bits[0] { for x := range bits[0] {
upper := false upper := false
@ -55,7 +55,7 @@ func qrCodeToString(bits [][]bool, colorize bool) string {
} }
} }
if colorize { if colorize {
s += TerminalForegroundReset + TerminalBackgroundReset s += ForegroundReset + BackgroundReset
} }
s += "\n" s += "\n"
} }

View file

@ -17,11 +17,11 @@
* along with Remote-Touchpad. If not, see <http://www.gnu.org/licenses/>. * along with Remote-Touchpad. If not, see <http://www.gnu.org/licenses/>.
*/ */
package main package terminal
const ( const (
TerminalForegroundBlack string = "\033[30m" ForegroundBlack string = "\033[30m"
TerminalForegroundReset string = "\033[39m" ForegroundReset string = "\033[39m"
TerminalBackgroundWhite string = "\033[47m" BackgroundWhite string = "\033[47m"
TerminalBackgroundReset string = "\033[49m" BackgroundReset string = "\033[49m"
) )

View file

@ -1,4 +1,4 @@
// +build !windows //go:build !windows
/* /*
* Copyright (c) 2018-2019 Unrud <unrud@outlook.com> * Copyright (c) 2018-2019 Unrud <unrud@outlook.com>
@ -19,17 +19,17 @@
* along with Remote-Touchpad. If not, see <http://www.gnu.org/licenses/>. * along with Remote-Touchpad. If not, see <http://www.gnu.org/licenses/>.
*/ */
package main package terminal
// #include <unistd.h> // #include <unistd.h>
import "C" import "C"
import "os" import "os"
func TerminalSupportsColor(fd uintptr) bool { func SupportsColor(fd uintptr) bool {
return C.isatty(C.int(fd)) != 0 return C.isatty(C.int(fd)) != 0
} }
func TerminalSetTitle(title string) bool { func SetTitle(title string) bool {
if C.isatty(C.int(os.Stdout.Fd())) != 0 { if C.isatty(C.int(os.Stdout.Fd())) != 0 {
os.Stdout.Write([]byte("\x1b]2;" + title + "\x07")) os.Stdout.Write([]byte("\x1b]2;" + title + "\x07"))
os.Stdout.Sync() os.Stdout.Sync()

View file

@ -17,7 +17,7 @@
* along with Remote-Touchpad. If not, see <http://www.gnu.org/licenses/>. * along with Remote-Touchpad. If not, see <http://www.gnu.org/licenses/>.
*/ */
package main package terminal
import ( import (
"syscall" "syscall"
@ -36,13 +36,13 @@ var (
setConsoleTitleProc = kernel32DLL.NewProc("SetConsoleTitleW") setConsoleTitleProc = kernel32DLL.NewProc("SetConsoleTitleW")
) )
func TerminalSupportsColor(fd uintptr) bool { func SupportsColor(fd uintptr) bool {
r, _, _ := setConsoleModeProc.Call(fd, uintptr(enableProcessedOuput| r, _, _ := setConsoleModeProc.Call(fd, uintptr(enableProcessedOuput|
enableWrapAtEolOutput|enableVirtualTerminalProcessing)) enableWrapAtEolOutput|enableVirtualTerminalProcessing))
return r != 0 return r != 0
} }
func TerminalSetTitle(title string) bool { func SetTitle(title string) bool {
r, _, _ := setConsoleTitleProc.Call( r, _, _ := setConsoleTitleProc.Call(
uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(title)))) uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(title))))
return r != 0 return r != 0

View file

@ -1,7 +1,5 @@
// +build !portal
/* /*
* Copyright (c) 2018 Unrud <unrud@outlook.com> * Copyright (c) 2024 Unrud <unrud@outlook.com>
* *
* This file is part of Remote-Touchpad. * This file is part of Remote-Touchpad.
* *
@ -21,8 +19,34 @@
package main package main
import "errors" import (
"embed"
"io/fs"
"log"
"mime"
)
func InitPortalBackend() (Backend, error) { //go:embed webdata/*
return nil, UnsupportedPlatformError{errors.New("disabled")} var webdataFSWithPrefix embed.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() {
var err error
webdataFS, err = fs.Sub(webdataFSWithPrefix, "webdata")
if err != nil {
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
View 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);
}
};

View 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
View 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
View 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
View 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

File diff suppressed because one or more lines are too long

66
webdata/app/socket.mjs Normal file
View 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
View 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
View 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);
}
}
}

View file

@ -1,506 +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;
var touchMoved = false;
var touchStart = 0;
var touchLastEnd = 0;
var touchReleasedCount = 0;
var ongoingTouches = [];
var moveXSum = 0;
var moveYSum = 0;
var scrollXSum = 0;
var scrollYSum = 0;
var dragging = false;
var draggingTimeout = null;
var scrolling = false;
function fullscreenEnabled() {
return (document.fullscreenEnabled ||
document.webkitFullscreenEnabled ||
document.mozFullScreenEnabled ||
document.msFullscreenEnabled ||
false);
}
function requestFullscreen(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);
}
}
function exitFullscreen() {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
} else if (document.msExitFullscreen) {
document.msExitFullscreen();
}
}
function fullscreenElement() {
return (document.fullscreenElement ||
document.webkitFullscreenElement ||
document.mozFullScreenElement ||
document.msFullscreenElement ||
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 calculatePointerAccelerationMult(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;
ws.send("b" + POINTER_BUTTON_LEFT + ";0");
}
function updateMoveAndScroll() {
var moveX = Math.trunc(moveXSum);
var moveY = Math.trunc(moveYSum);
if (Math.abs(moveX) >= 1 || Math.abs(moveY) >= 1) {
moveXSum -= moveX;
moveYSum -= moveY;
ws.send("m" + moveX + ";" + moveY);
}
var scrollX = Math.trunc(scrollXSum);
var scrollY = Math.trunc(scrollYSum);
if (Math.abs(scrollX) >= 1 || Math.abs(scrollY) >= 1) {
scrollXSum -= scrollX;
scrollYSum -= scrollY;
scrolling = true;
ws.send("s" + scrollX + ";" + scrollY);
}
}
function handleStart(evt) {
// Might get called multiple times for the same touches
if (ongoingTouches.length == 0) {
touchStart = evt.timeStamp;
touchMoved = false;
}
var touches = evt.changedTouches;
for (var i = 0; i < touches.length; i += 1) {
if (ongoingTouches.length == 0 && !touches[i].target.classList.contains("touchpad")) {
continue;
}
evt.preventDefault();
var touch = copyTouch(touches[i], evt.timeStamp);
var idx = ongoingTouchIndexById(touch.identifier);
if (idx < 0) {
ongoingTouches.push(touch);
} else {
ongoingTouches[idx] = touch;
}
touchLastEnd = 0;
if (!dragging) {
moveXSum = Math.trunc(moveXSum);
moveYSum = Math.trunc(moveYSum);
}
scrollXSum = Math.trunc(scrollXSum);
scrollYSum = Math.trunc(scrollYSum);
if (draggingTimeout != null) {
clearTimeout(draggingTimeout);
draggingTimeout = null;
dragging = true;
}
if (scrolling) {
ws.send("sf");
scrolling = false;
}
}
}
function handleEnd(evt) {
var touches = evt.changedTouches;
for (var i = 0; i < touches.length; i += 1) {
var idx = ongoingTouchIndexById(touches[i].identifier);
if (idx < 0) {
continue;
}
ongoingTouches.splice(idx, 1);
touchReleasedCount += 1;
touchLastEnd = evt.timeStamp;
if (scrolling) {
ws.send("sf");
scrolling = false;
}
}
if (touchReleasedCount > TOUCH_MOVE_THRESHOLD.length) {
touchMoved = true;
}
if (ongoingTouches.length == 0 && touchReleasedCount >= 1) {
if (dragging) {
dragging = false;
ws.send("b" + POINTER_BUTTON_LEFT + ";0");
}
if (!touchMoved && evt.timeStamp - touchStart < TOUCH_TIMEOUT) {
var button = 0;
if (touchReleasedCount == 1) {
button = POINTER_BUTTON_LEFT;
} else if (touchReleasedCount == 2) {
button = POINTER_BUTTON_RIGHT;
} else if (touchReleasedCount == 3) {
button = POINTER_BUTTON_MIDDLE;
}
ws.send("b" + button + ";1");
if (button == POINTER_BUTTON_LEFT) {
draggingTimeout = setTimeout(onDraggingTimeout, TOUCH_TIMEOUT);
} else {
ws.send("b" + button + ";0");
}
}
touchReleasedCount = 0;
}
}
function handleMove(evt) {
var sumX = 0;
var sumY = 0;
var touches = evt.changedTouches;
for (var i = 0; i < touches.length; i += 1) {
var idx = ongoingTouchIndexById(touches[i].identifier);
if (idx < 0) {
continue;
}
if (!touchMoved) {
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 - touchStart >= TOUCH_TIMEOUT) {
touchMoved = 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 * calculatePointerAccelerationMult(Math.abs(dx) / timeDelta * 1000);
sumY += dy * calculatePointerAccelerationMult(Math.abs(dy) / timeDelta * 1000);
ongoingTouches[idx].pageX = touches[i].pageX;
ongoingTouches[idx].pageY = touches[i].pageY;
ongoingTouches[idx].timeStamp = evt.timeStamp;
}
if (touchMoved && evt.timeStamp - touchLastEnd >= TOUCH_TIMEOUT) {
if (ongoingTouches.length == 1 || dragging) {
moveXSum += sumX;
moveYSum += sumY;
} else if (ongoingTouches.length == 2) {
scrollXSum -= sumX;
scrollYSum -= sumY;
}
updateMoveAndScroll();
}
}
function handleKeydown(evt) {
if (evt.ctrlKey || evt.altKey || evt.isComposing) {
return;
}
var key = {
"OS": KEY_SUPER,
"Super": KEY_SUPER,
"Meta": KEY_SUPER,
"Backspace": KEY_BACK_SPACE,
"Enter": KEY_RETURN,
"Delete": KEY_DELETE,
"Home": KEY_HOME,
"End": KEY_END,
"Left": KEY_LEFT,
"ArrowLeft": KEY_LEFT,
"Right": KEY_RIGHT,
"ArrowRight": KEY_RIGHT,
"Up": KEY_UP,
"ArrowUp": KEY_UP,
"Down": KEY_DOWN,
"ArrowDown": KEY_DOWN
}[evt.key];
if (key != undefined && !evt.shiftKey) {
evt.preventDefault();
ws.send("k" + key);
} else if (evt.key.length == 1) {
evt.preventDefault();
ws.send("t" + evt.key);
}
}
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 authenticated = false;
var scenes = document.querySelectorAll("body > .scene");
var opening = document.getElementById("opening");
var closed = document.getElementById("closed");
var pad = document.getElementById("pad");
var keys = document.getElementById("keys");
var keysScenes = keys.querySelectorAll(".scene");
var keyboard = document.getElementById("keyboard");
var fullscreenbutton = document.getElementById("fullscreenbutton");
var keyboardtext = document.getElementById("keyboardtext");
var activeScene;
var keysActiveName;
function showScene(scene) {
activeScene = scene;
if (fullscreenElement() && !scene.classList.contains("fullscreen")) {
exitFullscreen();
}
keyboardtext.value = "";
scenes.forEach(function(otherScene) {
otherScene.classList.toggle("hidden", otherScene != scene);
});
}
function showKeysScene(index) {
if (!Number.isInteger(index) || index < 0 || keysScenes.length <= index) {
index = 0;
}
sessionStorage.setItem(keysActiveName, index);
for (var i = 0; i < keysScenes.length; i += 1) {
keysScenes[i].classList.toggle("hidden", i != index);
}
}
function showKeys(name) {
showScene(keys);
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(keyboard);
keyboardtext.value = sessionStorage.getItem("keyboard") || "";
keyboardtext.focus();
if (history.state != "keyboard") {
history.pushState("keyboard", "");
}
}
keyboardtext.oninput = function() {
sessionStorage.setItem("keyboard", keyboardtext.value);
};
showScene(opening);
var wsURL = new URL("ws", location.href);
wsURL.protocol = wsURL.protocol == "http:" ? "ws:" : "wss:";
ws = new WebSocket(wsURL);
ws.onmessage = function(evt) {
if (authenticated) {
ws.close();
return;
}
ws.send(challengeResponse(evt.data));
authenticated = true;
window.onpopstate();
};
ws.onclose = function() {
authenticated = false;
showScene(closed);
};
document.getElementById("keysbutton").addEventListener("click", function() {
showKeys();
});
document.getElementById("keyboardbutton").addEventListener("click", function() {
showKeyboard();
});
if (!fullscreenEnabled()) {
fullscreenbutton.classList.add("hidden");
}
fullscreenbutton.addEventListener("click", function() {
if (fullscreenElement()) {
exitFullscreen();
} else {
requestFullscreen(document.documentElement, {navigationUI: "hide"});
}
});
document.getElementById("switchbutton").addEventListener("click", function() {
var keysIndex = 0;
for (var i = 0; i < keysScenes.length; i += 1) {
if (!keysScenes[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() {
ws.send("k" + o.key);
});
});
document.getElementById("keyboardkeysbutton").addEventListener("click", function() {
showKeys("keyboard");
});
document.getElementById("sendbutton").addEventListener("click", function() {
if (keyboardtext.value) {
ws.send("t" + keyboardtext.value);
keyboardtext.value = "";
keyboardtext.oninput();
}
history.back();
});
window.onpopstate = function() {
if (authenticated) {
if ((history.state || "").split(":")[0] == "keys") {
showKeys(history.state.substr("keys:".length));
} else if (history.state == "keyboard") {
showKeyboard();
} else {
showScene(pad);
}
}
};
document.getElementById("reloadbutton").addEventListener("click", function() {
location.reload();
});
document.querySelectorAll(".backbutton").forEach(function(button) {
button.addEventListener("click", function() {
history.back();
});
});
document.addEventListener("touchstart", handleStart);
document.addEventListener("touchend", handleEnd);
document.addEventListener("touchcancel", handleEnd);
document.addEventListener("touchmove", handleMove);
window.addEventListener("keydown", function(evt) {
if (activeScene && activeScene.classList.contains("key")) {
handleKeydown(evt);
}
});
});

View file

@ -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,58 +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 touchpad key fullscreen hidden">
<p id="padlabel" class="touchpad">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 id="keyboardtext" 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 touchpad key fullscreen hidden">
<div class="scene touchpad"> <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 touchpad"> <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>
<button class="top right" onclick="app.setKeysPage(1, true)"></button>
<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>

View file

@ -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,41 +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%;
}
#padlabel {
color: gray; color: gray;
text-shadow: text-shadow:
-1px -1px 0 black, -1px -1px 0 black,
@ -86,43 +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) {
#padlabel { p.background {
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;
@ -132,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; flex-direction: column;
border-left: none;
} }
#keyboard .buttons > button:last-of-type { #text-input .buttons > button {
flex-grow: 1; border-top: none;
border-right: none; border-right: none;
} }
@media (min-aspect-ratio: 4/3) { #text-input .buttons > button:last-child {
#keyboard { border-bottom: none;
flex-direction: row; }
}
#keyboard .buttons { /* 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; flex-direction: column;
} }
#keyboard .buttons > button { #text-input .buttons {
border-top: none; flex-direction: row;
border-right: none;
border-left: 1px solid black;
} }
#keyboard .buttons > button:last-of-type { #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; border-left: none;
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;
}
#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;
}

View file

@ -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);

View file

@ -1,7 +1,5 @@
// +build !windows
/* /*
* Copyright (c) 2018 Unrud <unrud@outlook.com> * Copyright (c) 2024 Unrud <unrud@outlook.com>
* *
* This file is part of Remote-Touchpad. * This file is part of Remote-Touchpad.
* *
@ -21,8 +19,24 @@
package main package main
import "errors" import (
"fmt"
"io/fs"
"path/filepath"
"testing"
)
func InitWindowsBackend() (Backend, error) { func TestWebdataTypesCompleteness(t *testing.T) {
return nil, UnsupportedPlatformError{errors.New("disabled")} 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)
}
} }