Compare commits

..

56 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
23 changed files with 846 additions and 207 deletions

View file

@ -23,7 +23,7 @@ jobs:
with:
go-version: stable
check-latest: true
- run: sudo apt install libxrandr-dev libxtst-dev
- run: sudo apt install libxrandr-dev libxtst-dev libxt-dev
if: ${{ contains(matrix.tags, 'x11') }}
- name: Go Format
@ -38,10 +38,6 @@ jobs:
- name: Go Build
run: go build ./...
- name: Go Test
run: go test ./...
if: ${{ matrix.os != 'linux' }}
- 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,52 @@
# 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

View file

@ -12,8 +12,45 @@ Supports Flatpak's RemoteDesktop portal (for Wayland), Windows and X11.
* [Snap](https://snapcraft.io/remote-touchpad)
* [Windows](https://github.com/Unrud/remote-touchpad/releases/latest)
* Golang:
* Portal & uinput & X11: `go install -tags portal,uinput,x11 github.com/unrud/remote-touchpad@latest`
* Windows: `go install github.com/unrud/remote-touchpad@latest`
* Portal & uinput & X11:
```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

View file

@ -17,8 +17,89 @@
<launchable type="desktop-id">com.github.unrud.RemoteTouchpad.desktop</launchable>
<url type="bugtracker">https://github.com/Unrud/remote-touchpad/issues</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>
<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>
@ -292,16 +373,16 @@
</releases>
<screenshots>
<screenshot type="default">
<image>https://raw.githubusercontent.com/Unrud/remote-touchpad/v1.4.2/screenshots/1.png</image>
<image>https://raw.githubusercontent.com/Unrud/remote-touchpad/v1.5.4/screenshots/1.png</image>
</screenshot>
<screenshot>
<image>https://raw.githubusercontent.com/Unrud/remote-touchpad/v1.4.2/screenshots/2.png</image>
<image>https://raw.githubusercontent.com/Unrud/remote-touchpad/v1.5.4/screenshots/2.png</image>
</screenshot>
<screenshot>
<image>https://raw.githubusercontent.com/Unrud/remote-touchpad/v1.4.2/screenshots/3.png</image>
<image>https://raw.githubusercontent.com/Unrud/remote-touchpad/v1.5.4/screenshots/3.png</image>
</screenshot>
<screenshot>
<image>https://raw.githubusercontent.com/Unrud/remote-touchpad/v1.4.2/screenshots/4.png</image>
<image>https://raw.githubusercontent.com/Unrud/remote-touchpad/v1.5.4/screenshots/4.png</image>
</screenshot>
</screenshots>
<content_rating type="oars-1.1"/>

View file

@ -1,6 +1,6 @@
app-id: com.github.unrud.RemoteTouchpad
runtime: org.freedesktop.Platform
runtime-version: '22.08'
runtime-version: '25.08'
sdk: org.freedesktop.Sdk
command: remote-touchpad
sdk-extensions:
@ -8,19 +8,17 @@ sdk-extensions:
finish-args:
- --socket=fallback-x11
- --socket=wayland
- --share=ipc # Required by Flathub linter
- --share=network
modules:
- name: remote-touchpad
sources:
- type: git
path: ..
- type: shell
commands:
- . /usr/lib/sdk/golang/enable.sh; go mod vendor -e
- type: git
url: https://github.com/godbus/dbus
tag: v5.1.0
commit: e523abc905595cf17fb0001a7d77eaaddfaa216d
tag: v5.2.2
commit: a8ac15ba63645f02ffd57f4b443203279ab40b30
dest: vendor/github.com/godbus/dbus/v5
- type: git
url: https://github.com/skip2/go-qrcode
@ -28,9 +26,17 @@ modules:
dest: vendor/github.com/skip2/go-qrcode
- type: git
url: https://github.com/golang/net
tag: v0.10.0
commit: daac0cec0cf964a628a29bb4b82940c225b921ed
tag: v0.57.0
commit: b8f09f6f062ceb4531b7af4bd17a5c8fe9c4b2b5
dest: vendor/golang.org/x/net
- type: git
url: https://github.com/golang/sys
tag: v0.47.0
commit: 9e7e939dcafac07e8ab4cffa6e5fc74908413f00
dest: vendor/golang.org/x/sys
- type: file
path: modules.txt
dest: vendor
buildsystem: simple
build-commands:
- . /usr/lib/sdk/golang/enable.sh; go build -tags portal,x11

17
flatpak/modules.txt Normal file
View file

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

10
go.mod
View file

@ -1,10 +1,12 @@
module github.com/unrud/remote-touchpad
go 1.18
go 1.25.5
require (
github.com/bendahl/uinput v1.6.2
github.com/godbus/dbus/v5 v5.1.0
github.com/bendahl/uinput v1.7.0
github.com/godbus/dbus/v5 v5.2.2
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
golang.org/x/net v0.10.0
golang.org/x/net v0.57.0
)
require golang.org/x/sys v0.47.0 // indirect

14
go.sum
View file

@ -1,8 +1,10 @@
github.com/bendahl/uinput v1.6.2 h1:tIz52QyKDx1i1nObUkts3AZa/bULfLhPA5a+xKGlRPI=
github.com/bendahl/uinput v1.6.2/go.mod h1:Np7w3DINc9wB83p12fTAM3DPPhFnAKP0WTXRqCQJ6Z8=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/bendahl/uinput v1.7.0 h1:nA4fm8Wu8UYNOPykIZm66nkWEyvxzfmJ8YC02PM40jg=
github.com/bendahl/uinput v1.7.0/go.mod h1:Np7w3DINc9wB83p12fTAM3DPPhFnAKP0WTXRqCQJ6Z8=
github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=

View file

@ -74,7 +74,8 @@ func findDefaultHost() string {
continue
}
for _, linkLocalPrefix := range []string{
"169.254.", "fe8", "fe9", "fea", "feb"} {
"169.254.", "fe8", "fe9", "fea", "feb",
} {
if strings.HasPrefix(ip.String(), linkLocalPrefix) {
addIP(20, ip)
continue addrs

View file

@ -21,8 +21,10 @@ package inputcontrol
import "sort"
type PointerButton int
type Key int
type (
PointerButton int
Key int
)
const (
PointerButtonLeft PointerButton = iota

View file

@ -22,8 +22,19 @@
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"
)
@ -34,6 +45,8 @@ const (
btnReleased uint32 = 0
btnPressed uint32 = 1
untilRevoked uint32 = 2
// linux/input-event-codes.h
btnLeft int32 = 0x110
btnRight int32 = 0x111
@ -42,7 +55,7 @@ const (
type portalController struct {
bus *dbus.Conn
remoteDesktop dbus.BusObject
portalDesktop dbus.BusObject
sessionHandle dbus.ObjectPath
}
@ -69,85 +82,209 @@ func InitPortalController() (Controller, error) {
if err != nil {
return nil, &UnsupportedPlatformError{err}
}
remoteDesktop := bus.Object("org.freedesktop.portal.Desktop",
portalDesktop := bus.Object("org.freedesktop.portal.Desktop",
"/org/freedesktop/portal/desktop")
availableDeviceTypesV, err := remoteDesktop.GetProperty(
remoteDesktopVersionV, err := portalDesktop.GetProperty(
"org.freedesktop.portal.RemoteDesktop.version")
if err != nil {
return nil, &UnsupportedPlatformError{
fmt.Errorf("getting 'version' failed: %w", err),
}
}
remoteDesktopVersion, ok := remoteDesktopVersionV.Value().(uint32)
if !ok {
return nil, &UnsupportedPlatformError{
errors.New("unexpected 'version' type"),
}
}
restoreTokenStore, err := func() (*secretStore, error) {
if remoteDesktopVersion < 2 {
return nil, nil
}
cacheDirectory, err := os.UserCacheDir()
if err != nil {
return nil, err
}
if err := os.MkdirAll(cacheDirectory, 0o700); err != nil {
return nil, err
}
secret, err := retrieveSecret(bus)
if err != nil {
return nil, err
}
return newSecretStore(secret,
filepath.Join(cacheDirectory, "remote-touchpad.portal-restore-token.bin"))
}()
if err != nil {
log.Printf("Skipping restore token: %v", err)
}
availableDeviceTypesV, err := portalDesktop.GetProperty(
"org.freedesktop.portal.RemoteDesktop.AvailableDeviceTypes")
if err != nil {
return nil, &UnsupportedPlatformError{err}
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")}
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")}
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)
}
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{err}
}
if result != 0 {
return nil, &UnsupportedPlatformError{
fmt.Errorf("Calling 'CreateSession' failed (%v)", result)}
fmt.Errorf("calling 'CreateSession' failed: %w", err),
}
sessionHandleV, ok := outVardict["session_handle"]
}
sessionHandleString, ok := createSessionResults["session_handle"].Value().(string)
if !ok {
return nil, &UnsupportedPlatformError{
errors.New("'session_handle' missing from 'CreateSession' return value")}
errors.New("unexpected 'session_handle' type in 'CreateSession' return value"),
}
sessionHandleS, ok := sessionHandleV.Value().(string)
if !ok {
return nil, &UnsupportedPlatformError{
errors.New("unexpected 'session_handle' type in 'CreateSession' return value")}
}
sessionHandle := dbus.ObjectPath(sessionHandleS)
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)
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{err}
}
if result != 0 {
return nil, &UnsupportedPlatformError{
fmt.Errorf("Calling 'SelectDevices' failed (%v)", result)}
fmt.Errorf("calling 'SelectDevices' failed: %w", err),
}
inVardict = make(map[string]dbus.Variant)
result, outVardict, err = getResponse(bus, remoteDesktop,
"org.freedesktop.portal.RemoteDesktop.Start", 0, sessionHandle, "", inVardict)
}
startResponseStatus, startResults, err := getResponse(bus, portalDesktop,
"org.freedesktop.portal.RemoteDesktop.Start", 0,
sessionHandle, "", map[string]dbus.Variant{},
)
if err != nil {
return nil, &UnsupportedPlatformError{err}
return nil, &UnsupportedPlatformError{
fmt.Errorf("calling 'Start' failed: %w", err),
}
if result != 0 {
}
if startResponseStatus != 0 {
return nil, errors.New("keyboard or pointer access denied")
}
devicesV, ok := outVardict["devices"]
if !ok {
return nil, &UnsupportedPlatformError{
errors.New("'devices' missing from 'Start' return value")}
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 := devicesV.Value().(uint32)
}
devices, ok := startResults["devices"].Value().(uint32)
if !ok {
return nil, &UnsupportedPlatformError{
errors.New("unexpected 'devices' type in 'Start' return value")}
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, remoteDesktop: remoteDesktop,
sessionHandle: sessionHandle}, nil
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) {
flags dbus.Flags, args ...interface{},
) (uint32, map[string]dbus.Variant, error) {
ch := make(chan *dbus.Signal, 512)
bus.Signal(ch)
defer bus.RemoveSignal(ch)
@ -159,32 +296,39 @@ func getResponse(bus *dbus.Conn, object dbus.BusObject, method string,
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")
return 0, nil, fmt.Errorf("unexpected 'Response' return length (%d)", len(s.Body))
}
result, ok := s.Body[0].(uint32)
responseStatus, ok := s.Body[0].(uint32)
if !ok {
return 0, nil, errors.New("unexpected 'Response' return type")
}
outVardict, ok := s.Body[1].(map[string]dbus.Variant)
results, ok := s.Body[1].(map[string]dbus.Variant)
if !ok {
return 0, nil, errors.New("unexpected 'Response' return type")
}
return result, outVardict, nil
return responseStatus, results, nil
}
}
}
func checkResponse(responseStatus uint32, results map[string]dbus.Variant, err error) (map[string]dbus.Variant, error) {
if err == nil && responseStatus != 0 {
err = fmt.Errorf("unexpected 'Response' status (%d)", responseStatus)
}
return results, err
}
func (p *portalController) Close() error {
return p.bus.Close()
}
func (p *portalController) keyboardKeys(keys []Keysym) error {
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 {
if err := p.portalDesktop.Call(
"org.freedesktop.portal.RemoteDesktop.NotifyKeyboardKeysym", 0,
p.sessionHandle, map[string]dbus.Variant{}, keysym, state,
).Store(); err != nil {
return err
}
}
@ -229,28 +373,27 @@ func (p *portalController) PointerButton(button PointerButton, press bool) error
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 {
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 {
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 {
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 {
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 {
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

@ -25,10 +25,11 @@ package inputcontrol
import (
"errors"
"fmt"
"github.com/bendahl/uinput"
"log"
"os"
"time"
"github.com/bendahl/uinput"
)
const shiftKeysDelay time.Duration = 50 * time.Millisecond

View file

@ -22,6 +22,7 @@
package inputcontrol
// #cgo LDFLAGS: -lX11 -lXrandr -lXtst
// #include <stdlib.h>
// #include <X11/Xlib.h>
// #include <X11/Intrinsic.h>
// #include <X11/extensions/Xrandr.h>
@ -31,9 +32,12 @@ package inputcontrol
// return DefaultRootWindow(dpy);
// }
import "C"
import (
"errors"
"fmt"
"maps"
"slices"
"strings"
"sync"
"time"
@ -41,12 +45,18 @@ import (
)
const (
keyboardMappingDelay time.Duration = 500 * time.Millisecond
keyboardMappingDelay time.Duration = 200 * time.Millisecond
scrollDiv int = 20
)
var modifierIndices [6]uint = [...]uint{C.ShiftMapIndex, C.Mod1MapIndex,
C.Mod2MapIndex, C.Mod3MapIndex, C.Mod4MapIndex, C.Mod5MapIndex}
var modifierIndexes = [...]int{
C.ShiftMapIndex,
C.Mod1MapIndex,
C.Mod2MapIndex,
C.Mod3MapIndex,
C.Mod4MapIndex,
C.Mod5MapIndex,
}
type x11Controller struct {
display *C.Display
@ -62,18 +72,31 @@ func InitX11Controller() (Controller, error) {
display := C.XOpenDisplay(nil)
if display == nil {
return nil, &UnsupportedPlatformError{
errors.New("failed to connect to X server")}
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")}
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
@ -101,53 +124,68 @@ func (p *x11Controller) Close() error {
return nil
}
func (p *x11Controller) findEmptyKeycodeLocked() (C.KeyCode, C.int, error) {
func (p *x11Controller) findEmptyKeycodeLocked() (keycode C.KeyCode, keysymsPerKeycode int, err error) {
var minKeycodes, maxKeycodes C.int
C.XDisplayKeycodes(p.display, &minKeycodes, &maxKeycodes)
var keysymsPerKeycode C.int
keysyms := C.XGetKeyboardMapping(p.display, C.KeyCode(minKeycodes),
maxKeycodes-minKeycodes+1, &keysymsPerKeycode)
var keysymsPerKeycodeC C.int
keysyms := C.XGetKeyboardMapping(
p.display,
C.KeyCode(minKeycodes),
maxKeycodes-minKeycodes+1,
&keysymsPerKeycodeC,
)
if keysyms == nil {
return 0, 0, errors.New("failed to get keyboard mapping")
}
defer C.XFree(unsafe.Pointer(keysyms))
keysymsPerKeycode = int(keysymsPerKeycodeC)
keycodes:
for keycode := C.KeyCode(minKeycodes); keycode <= C.KeyCode(maxKeycodes); keycode++ {
for i := 0; i < int(keysymsPerKeycode); i++ {
keysymsIndex := int(keycode-
C.KeyCode(minKeycodes))*int(keysymsPerKeycode) + i
keysym := *(*C.KeySym)(unsafe.Pointer(uintptr(unsafe.Pointer(keysyms)) +
uintptr(keysymsIndex)*unsafe.Sizeof(*keysyms)))
for keycode := int(minKeycodes); keycode <= int(maxKeycodes); keycode++ {
for i := range keysymsPerKeycode {
keysymsIndex := (keycode-int(minKeycodes))*keysymsPerKeycode + i
keysym := *(*C.KeySym)(unsafe.Pointer(
uintptr(unsafe.Pointer(keysyms)) +
uintptr(keysymsIndex)*unsafe.Sizeof(*keysyms),
))
if keysym != 0 {
continue keycodes
}
}
return keycode, keysymsPerKeycode, nil
return C.KeyCode(keycode), keysymsPerKeycode, nil
}
return 0, 0, errors.New("no empty keycode found")
}
func (p *x11Controller) changeKeyMappingLocked(keysymsPerKeycode C.int,
keycode C.KeyCode, keysym Keysym) {
func (p *x11Controller) changeKeyMappingLocked(
keycode C.KeyCode, keysymsPerKeycode int, keysym Keysym,
) {
keycodeMapping := make([]C.KeySym, keysymsPerKeycode)
for i := range keycodeMapping {
keycodeMapping[i] = C.KeySym(keysym)
}
C.XChangeKeyboardMapping(p.display, C.int(keycode), keysymsPerKeycode,
(*C.KeySym)(unsafe.Pointer(&keycodeMapping[0])), 1)
C.XFlush(p.display)
C.XChangeKeyboardMapping(
p.display,
C.int(keycode),
C.int(keysymsPerKeycode),
(*C.KeySym)(unsafe.Pointer(&keycodeMapping[0])),
1,
)
C.XSync(p.display, C.False)
}
func (p *x11Controller) getModKeycodesLocked() map[uint]C.KeyCode {
modKeymap := C.XGetModifierMapping(p.display)
defer C.XFreeModifiermap(modKeymap)
modKeycodes := make(map[uint]C.KeyCode)
for _, modIndex := range modifierIndices {
for i := 0; i < int(modKeymap.max_keypermod); i++ {
keycode := *(*C.KeyCode)(unsafe.Pointer(uintptr(unsafe.Pointer(modKeymap.modifiermap)) +
uintptr(uint(modIndex)*uint(modKeymap.max_keypermod)+uint(i))))
for _, modIndex := range modifierIndexes {
for i := range int(modKeymap.max_keypermod) {
keycodeIndex := modIndex*int(modKeymap.max_keypermod) + i
keycode := *(*C.KeyCode)(unsafe.Pointer(
uintptr(unsafe.Pointer(modKeymap.modifiermap)) +
uintptr(keycodeIndex)*unsafe.Sizeof(*modKeymap.modifiermap),
))
if keycode != 0 {
modKeycodes[1<<uint(modIndex)] = keycode
modKeycodes[1<<modIndex] = keycode
break
}
}
@ -155,50 +193,39 @@ func (p *x11Controller) getModKeycodesLocked() map[uint]C.KeyCode {
return modKeycodes
}
func (p *x11Controller) findKeycodeLocked(keyboard C.XkbDescPtr,
modKeycodes map[uint]C.KeyCode, activeMods C.uint,
keysym Keysym) (C.KeyCode, C.uint) {
keycode := C.XKeysymToKeycode(p.display, C.KeySym(keysym))
func (p *x11Controller) findKeycodeLocked(
keyboard C.XkbDescPtr, modKeycodes map[uint]C.KeyCode, activeMods uint, keysym Keysym,
) (keycode C.KeyCode, mods uint) {
keycode = C.XKeysymToKeycode(p.display, C.KeySym(keysym))
if keycode == 0 {
return 0, 0
}
var alwaysActiveMods C.uint
for modIndex := uint(0); modIndex < 8; modIndex++ {
mod := uint(1) << modIndex
if _, modAvailable := modKeycodes[mod]; !modAvailable {
alwaysActiveMods |= activeMods & C.uint(mod)
for activeModBitmap := range uint(1) << len(modKeycodes) {
mods := activeMods
for i, mod := range slices.Sorted(maps.Keys(modKeycodes)) {
if activeModBitmap&(1<<i) != 0 {
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)
C.XkbTranslateKeyCode(keyboard, keycode, C.uint(mods), &retMods, &retKeysym)
if retKeysym == C.KeySym(keysym) {
return keycode, mods
}
}
}
return 0, 0
}
func (p *x11Controller) sendModsLocked(modKeycodes map[uint]C.KeyCode, mods C.uint,
press bool) {
func (p *x11Controller) sendModsLocked(
modKeycodes map[uint]C.KeyCode, mods uint, press bool,
) {
var pressC C.int = C.False
if press {
pressC = C.True
}
for mod, keycode := range modKeycodes {
if mods&C.uint(mod) != 0 {
if mods&mod != 0 {
C.XTestFakeKeyEvent(p.display, C.uint(keycode), pressC, 0)
}
}
@ -219,17 +246,22 @@ func (p *x11Controller) keyboardKeys(keys []Keysym) error {
C.XkbCompatMapMask|C.XkbGeometryMask, C.XkbUseCoreKbd)
defer C.XkbFreeKeyboard(keyboard, C.XkbAllComponentsMask, C.True)
var emptyKeycode C.KeyCode
var keysymsPerKeycode C.int
var keysymsPerKeycode int
for _, keysym := range keys {
var root, child C.Window
var rootX, rootY, x, y C.int
var activeMods C.uint
C.XSync(p.display, C.False)
C.XQueryPointer(p.display, rootWindow, &root, &child, &rootX, &rootY,
&x, &y, &activeMods)
keycode, mods := p.findKeycodeLocked(keyboard, modKeycodes, activeMods,
keysym)
var pressMods, releaseMods C.uint
var activeModsC C.uint
C.XQueryPointer(
p.display,
rootWindow,
&root, &child,
&rootX, &rootY,
&x, &y,
&activeModsC,
)
activeMods := uint(activeModsC)
keycode, mods := p.findKeycodeLocked(keyboard, modKeycodes, activeMods, keysym)
var pressMods, releaseMods uint
if keycode == 0 {
if emptyKeycode == 0 {
var err error
@ -237,12 +269,10 @@ func (p *x11Controller) keyboardKeys(keys []Keysym) error {
if err != nil {
return err
}
defer p.changeKeyMappingLocked(keysymsPerKeycode, emptyKeycode, 0)
defer p.changeKeyMappingLocked(emptyKeycode, keysymsPerKeycode, 0)
}
keycode = emptyKeycode
p.changeKeyMappingLocked(keysymsPerKeycode, keycode, keysym)
// race condition!
time.Sleep(keyboardMappingDelay)
p.changeKeyMappingLocked(keycode, keysymsPerKeycode, keysym)
} else {
pressMods = mods & ^activeMods
releaseMods = activeMods & ^mods
@ -253,7 +283,7 @@ func (p *x11Controller) keyboardKeys(keys []Keysym) error {
C.XTestFakeKeyEvent(p.display, C.uint(keycode), C.False, 0)
p.sendModsLocked(modKeycodes, pressMods, false)
p.sendModsLocked(modKeycodes, releaseMods, true)
C.XFlush(p.display)
C.XSync(p.display, C.False)
if keycode == emptyKeycode {
// race condition!
time.Sleep(keyboardMappingDelay)
@ -297,7 +327,7 @@ func (p *x11Controller) sendButton(button uint, press bool) error {
pressC = C.True
}
C.XTestFakeButtonEvent(p.display, C.uint(button), pressC, 0)
C.XFlush(p.display)
C.XSync(p.display, C.False)
return nil
}
@ -321,7 +351,7 @@ func (p *x11Controller) PointerMove(deltaX, deltaY int) error {
return errors.New("X server connection closed")
}
C.XTestFakeRelativeMotionEvent(p.display, C.int(deltaX), C.int(deltaY), 0)
C.XFlush(p.display)
C.XSync(p.display, C.False)
return nil
}
@ -342,7 +372,7 @@ func (p *x11Controller) PointerScroll(deltaHorizontal, deltaVertical int, finish
buttonHorizontal = 6
stepsHorizontal = -stepsHorizontal
}
for i := 0; i < stepsHorizontal; i++ {
for range stepsHorizontal {
if err := p.sendButton(buttonHorizontal, true); err != nil {
return err
}
@ -355,7 +385,7 @@ func (p *x11Controller) PointerScroll(deltaHorizontal, deltaVertical int, finish
buttonVertical = 4
stepsVertical = -stepsVertical
}
for i := 0; i < stepsVertical; i++ {
for range stepsVertical {
if err := p.sendButton(buttonVertical, true); err != nil {
return err
}

View file

@ -27,9 +27,6 @@ import (
"errors"
"flag"
"fmt"
"github.com/unrud/remote-touchpad/inputcontrol"
"github.com/unrud/remote-touchpad/terminal"
"golang.org/x/net/websocket"
"log"
mathrand "math/rand"
"net"
@ -39,6 +36,10 @@ import (
"strings"
"time"
"unicode/utf8"
"github.com/unrud/remote-touchpad/inputcontrol"
"github.com/unrud/remote-touchpad/terminal"
"golang.org/x/net/websocket"
)
const (
@ -47,7 +48,7 @@ const (
authenticationRateBurst int = 10
challengeLength int = 8
defaultBind string = ":0"
version string = "1.4.2"
version string = "1.5.4"
prettyAppName string = "Remote Touchpad"
)

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:
# 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
license: GPL-3.0+
grade: stable
adopt-info: remote-touchpad
base: core20
base: core24
confinement: strict
platforms:
amd64:
arm64:
apps:
remote-touchpad:
command: bin/remote-touchpad
@ -26,7 +30,7 @@ apps:
- desktop
- x11
common-id: com.github.unrud.RemoteTouchpad
desktop: share/applications/com.github.unrud.RemoteTouchpad.desktop
desktop: usr/share/applications/com.github.unrud.RemoteTouchpad.desktop
parts:
remote-touchpad:
@ -36,6 +40,8 @@ parts:
- x11
source: .
source-type: git
build-snaps:
- go
build-packages:
- gcc
- libc6-dev
@ -46,14 +52,10 @@ parts:
- libxt6
- libxrandr2
- libxtst6
override-pull: |
snapcraftctl pull
# WORKAROUND: Point icon directly to SVG otherwise snapcraft can't find it
sed -e 's|Icon=com.github.unrud.RemoteTouchpad|Icon=/usr/share/icons/hicolor/scalable/apps/com.github.unrud.RemoteTouchpad.svg|' -i desktop/com.github.unrud.RemoteTouchpad.desktop
override-build: |
snapcraftctl build
install -Dm0755 -t "${SNAPCRAFT_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 "${SNAPCRAFT_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
craftctl default
install -Dm0755 -t "${CRAFT_PART_INSTALL}/bin" desktop/remote-touchpad-wait-on-error
install -Dm0644 -t "${CRAFT_PART_INSTALL}/usr/share/metainfo" desktop/com.github.unrud.RemoteTouchpad.metainfo.xml
install -Dm0644 -t "${CRAFT_PART_INSTALL}/usr/share/applications" desktop/com.github.unrud.RemoteTouchpad.desktop
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]

View file

@ -1,19 +1,52 @@
/*
* Copyright (c) 2024 Unrud <unrud@outlook.com>
*
* This file is part of Remote-Touchpad.
*
* Remote-Touchpad is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Remote-Touchpad is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Remote-Touchpad. If not, see <http://www.gnu.org/licenses/>.
*/
package main
import (
"embed"
"io/fs"
"log"
"mime"
)
//go:embed webdata/*
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)
}
}
}

View file

@ -57,12 +57,13 @@ export default class UI {
(target) => target.classList.contains("touch-input"));
document.addEventListener("mousedown", this.#handleMousedown.bind(this));
document.addEventListener("touchend", this.#handleTouchend.bind(this));
textInput.addEventListener("input", this.#handleTextInput.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();
@ -97,8 +98,13 @@ export default class UI {
}
}
#handleTextInput() {
#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) {
@ -112,11 +118,11 @@ export default class UI {
// normalize line endings
const text = textInput.value.replace(/\r\n?/g, "\n");
this.#inputController.keyboardText(text);
textInput.value = "";
this.#handleTextInput();
}
this.#updateTextInput("");
} else {
history.back();
}
}
#showScene(scene) {
this.#activeScene = scene;
@ -159,8 +165,7 @@ export default class UI {
showTextInput() {
this.#showScene(textInputScene);
textInput.value = sessionStorage.getItem("text-input") || "";
textInput.focus();
this.#updateTextInput(sessionStorage.getItem("text-input") || "");
if (history.state != "text-input") {
history.pushState("text-input", "");
}

View file

@ -1,7 +1,7 @@
<!DOCTYPE html>
<html lang="en">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=0">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no, interactive-widget=resizes-content">
<script src="app/main.mjs" type="module"></script>
<title>Remote Touchpad</title>
<link href="main.css" media="screen" rel="stylesheet">
@ -28,7 +28,7 @@
<textarea class="grow" cols=1 rows=1></textarea>
<div class="buttons">
<button onclick="app.showKeys('text-input', 1)"></button>
<button class="grow" id="send-text"></button>
<button class="grow" id="send-text" data-send-content="➣" data-back-content="⯇"></button>
</div>
</div>
@ -95,6 +95,6 @@
<button class="top left" onclick="history.back()"></button>
</div>
<div id="mouse" class="scene allow-keyboard allow-fullscreen">
<div id="mouse" class="scene keyboard-input allow-fullscreen">
<p class="background">Mouse</p>
</div>

View file

@ -153,6 +153,10 @@ textarea {
padding: 0 0.5rem;
}
textarea:focus {
outline: none;
}
#keys {
padding: 0;
justify-content: end;
@ -177,33 +181,48 @@ textarea {
align-items: stretch;
padding: 0;
gap: 0;
flex-direction: row;
}
#text-input .buttons {
flex-direction: column;
}
#text-input .buttons > button {
border-bottom: none;
border-top: none;
border-right: none;
}
#text-input .buttons > button:first-child {
border-left: none;
#text-input .buttons > button:last-child {
border-bottom: none;
}
@media (min-aspect-ratio: 4/3) {
#text-input {
flex-direction: row;
/* 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;
}
}
#text-input .buttons {
/* Portrait orientation (not for Safari on iOS) */
@supports not (-webkit-touch-callout: none) { @media (max-aspect-ratio: 4/3) {
#text-input {
flex-direction: column;
}
#text-input .buttons > button,
#text-input .buttons > button:first-child {
#text-input .buttons {
flex-direction: row;
}
#text-input .buttons > button {
border-top: 1px solid black;
border-left: 1px solid black;
border-bottom: none;
}
#text-input .buttons > button:first-child {
border-top: none;
border-left: none;
}
}
} }

42
webdata_test.go Normal file
View file

@ -0,0 +1,42 @@
/*
* Copyright (c) 2024 Unrud <unrud@outlook.com>
*
* This file is part of Remote-Touchpad.
*
* Remote-Touchpad is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Remote-Touchpad is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Remote-Touchpad. If not, see <http://www.gnu.org/licenses/>.
*/
package main
import (
"fmt"
"io/fs"
"path/filepath"
"testing"
)
func TestWebdataTypesCompleteness(t *testing.T) {
if err := fs.WalkDir(webdataFS, ".", func(_ string, d fs.DirEntry, err error) error {
if err != nil || d.IsDir() {
return err
}
ext := filepath.Ext(d.Name())
if _, haveType := webdataTypes[ext]; !haveType {
return fmt.Errorf("missing mime type for extension %#v", ext)
}
return nil
}); err != nil {
t.Fatal(err)
}
}