diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..9f3768a --- /dev/null +++ b/.github/workflows/test.yaml @@ -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' }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..07c477a --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +# Binaries +remote-touchpad + +# Systemd service state +.remote-touchpad.service.state + +# Logs and run files +.rts-log + +# Environment files +.env \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index d42f03f..e59454c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,78 @@ # 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 diff --git a/README.md b/README.md index 9cd73c2..e039950 100644 --- a/README.md +++ b/README.md @@ -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 & X11: `go install -tags portal,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 diff --git a/desktop/com.github.unrud.RemoteTouchpad.metainfo.xml b/desktop/com.github.unrud.RemoteTouchpad.metainfo.xml index 87ba074..113ef4d 100644 --- a/desktop/com.github.unrud.RemoteTouchpad.metainfo.xml +++ b/desktop/com.github.unrud.RemoteTouchpad.metainfo.xml @@ -17,8 +17,131 @@ com.github.unrud.RemoteTouchpad.desktop https://github.com/Unrud/remote-touchpad/issues https://github.com/Unrud/remote-touchpad - Unrud + https://github.com/Unrud/remote-touchpad + Unrud + + Unrud + + + +
    +
  • Improve X11 keyboard input
  • +
  • Update dependencies
  • +
+
+
+ + +
    +
  • Update dependencies
  • +
+
+
+ + +
    +
  • Update dependencies
  • +
+
+
+ + +
    +
  • Add restore token support for the remote desktop portal
  • +
  • Update dependencies
  • +
+
+
+ + +
    +
  • Update dependencies
  • +
+
+
+ + +
    +
  • Update dependencies
  • +
+
+
+ + +
    +
  • Prevent incorrect MIME types
  • +
+
+
+ + +
    +
  • Keep text input open after sending
  • +
  • Workaround for virtual keyboard covering buttons (Chromium & iOS)
  • +
  • Enable hardware keyboard input in mouse mode
  • +
+
+
+ + +
    +
  • Fix Xwayland detection on newer servers
  • +
  • Fix support for the KDE remote desktop portal
  • +
  • Set correct minimum version for Go
  • +
  • Update dependencies
  • +
+
+
+ + +
    +
  • Update dependencies
  • +
+
+
+ + +
    +
  • Fix Windows support
  • +
+
+
+ + +
    +
  • uinput: Fix vertical scroll direction
  • +
  • Update dependencies
  • +
+
+
+ + +
    +
  • Add feedback to buttons
  • +
  • Add numeric keypad
  • +
  • Prevent touch triggering mouse mode
  • +
  • Update dependencies
  • +
+
+
+ + +
    +
  • Initial support for uinput
  • +
  • Update dependencies
  • +
+
+
+ + +
    +
  • Improve XWayland detection
  • +
  • Update dependencies
  • +
  • Flatpak: Add Wayland socket to disable X11 socket
  • +
+
+
    @@ -250,16 +373,16 @@ - https://raw.githubusercontent.com/Unrud/remote-touchpad/v1.2.2/screenshots/1.png + https://raw.githubusercontent.com/Unrud/remote-touchpad/v1.5.4/screenshots/1.png - https://raw.githubusercontent.com/Unrud/remote-touchpad/v1.2.2/screenshots/2.png + https://raw.githubusercontent.com/Unrud/remote-touchpad/v1.5.4/screenshots/2.png - https://raw.githubusercontent.com/Unrud/remote-touchpad/v1.2.2/screenshots/3.png + https://raw.githubusercontent.com/Unrud/remote-touchpad/v1.5.4/screenshots/3.png - https://raw.githubusercontent.com/Unrud/remote-touchpad/v1.2.2/screenshots/4.png + https://raw.githubusercontent.com/Unrud/remote-touchpad/v1.5.4/screenshots/4.png diff --git a/flatpak/com.github.unrud.RemoteTouchpad.yaml b/flatpak/com.github.unrud.RemoteTouchpad.yaml index 8fecb8d..239160e 100644 --- a/flatpak/com.github.unrud.RemoteTouchpad.yaml +++ b/flatpak/com.github.unrud.RemoteTouchpad.yaml @@ -1,26 +1,24 @@ 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: - org.freedesktop.Sdk.Extension.golang finish-args: - - --socket=x11 - --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,8 +26,17 @@ modules: dest: vendor/github.com/skip2/go-qrcode - type: git url: https://github.com/golang/net - commit: 8be639271d50c5f812a860bf5b9c21d50f14e8d1 + 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 diff --git a/flatpak/modules.txt b/flatpak/modules.txt new file mode 100644 index 0000000..6092904 --- /dev/null +++ b/flatpak/modules.txt @@ -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 diff --git a/go.mod b/go.mod index d774cf9..5afd19d 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,12 @@ module github.com/unrud/remote-touchpad -go 1.17 +go 1.25.5 require ( - github.com/godbus/dbus/v5 v5.1.0 + github.com/bendahl/uinput v1.7.0 + github.com/godbus/dbus/v5 v5.2.2 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e - golang.org/x/net v0.0.0-20220923203811-8be639271d50 + golang.org/x/net v0.57.0 ) + +require golang.org/x/sys v0.47.0 // indirect diff --git a/go.sum b/go.sum index 792c79a..783b4df 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,10 @@ -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.0.0-20220923203811-8be639271d50 h1:vKyz8L3zkd+xrMeIaBsQ/MNVPVFSffdaU3ZyYlBGFnI= -golang.org/x/net v0.0.0-20220923203811-8be639271d50/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -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= +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= diff --git a/host.go b/host.go index 54874e5..622b3bf 100644 --- a/host.go +++ b/host.go @@ -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 diff --git a/inputcontrol/controller.go b/inputcontrol/controller.go index 7e9e768..111e937 100644 --- a/inputcontrol/controller.go +++ b/inputcontrol/controller.go @@ -21,8 +21,10 @@ package inputcontrol import "sort" -type PointerButton int -type Key int +type ( + PointerButton int + Key int +) const ( PointerButtonLeft PointerButton = iota @@ -70,11 +72,15 @@ func RegisterController(name string, init func() (Controller, error), priority i } type UnsupportedPlatformError struct { - err error + Err error } -func (e UnsupportedPlatformError) Error() string { - return e.err.Error() +func (e *UnsupportedPlatformError) Error() string { + return e.Err.Error() +} + +func (e *UnsupportedPlatformError) Unwrap() error { + return e.Err } type Controller interface { diff --git a/inputcontrol/controller_null.go b/inputcontrol/controller_null.go new file mode 100644 index 0000000..6188cce --- /dev/null +++ b/inputcontrol/controller_null.go @@ -0,0 +1,66 @@ +//go:build null + +/* + * Copyright (c) 2023 Unrud + * + * 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 . + */ + +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 +} diff --git a/inputcontrol/controller_portal.go b/inputcontrol/controller_portal.go index a9e503f..99ecf8b 100644 --- a/inputcontrol/controller_portal.go +++ b/inputcontrol/controller_portal.go @@ -22,10 +22,20 @@ 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" - "sync" ) const ( @@ -35,6 +45,8 @@ const ( btnReleased uint32 = 0 btnPressed uint32 = 1 + untilRevoked uint32 = 2 + // linux/input-event-codes.h btnLeft int32 = 0x110 btnRight int32 = 0x111 @@ -43,9 +55,8 @@ const ( type portalController struct { bus *dbus.Conn - remoteDesktop dbus.BusObject + portalDesktop dbus.BusObject sessionHandle dbus.ObjectPath - lock sync.RWMutex } func init() { @@ -55,7 +66,7 @@ func init() { func InitPortalController() (Controller, error) { bus, err := dbus.SessionBusPrivate() if err != nil { - return nil, UnsupportedPlatformError{err} + return nil, &UnsupportedPlatformError{err} } cleanupBus := true defer func() { @@ -65,91 +76,215 @@ func InitPortalController() (Controller, error) { }() err = bus.Auth(nil) if err != nil { - return nil, UnsupportedPlatformError{err} + return nil, &UnsupportedPlatformError{err} } err = bus.Hello() if err != nil { - return nil, UnsupportedPlatformError{err} + return nil, &UnsupportedPlatformError{err} } - remoteDesktop := bus.Object("org.freedesktop.portal.Desktop", + portalDesktop := bus.Object("org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop") - 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")} + 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")} + 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) + 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} + return nil, &UnsupportedPlatformError{ + fmt.Errorf("calling 'CreateSession' failed: %w", err), + } } - if result != 0 { - return nil, UnsupportedPlatformError{errors.New(fmt.Sprintf( - "Calling 'CreateSession' failed (%v)", result))} - } - sessionHandleV, ok := outVardict["session_handle"] + sessionHandleString, ok := createSessionResults["session_handle"].Value().(string) if !ok { - return nil, UnsupportedPlatformError{errors.New( - "'session_handle' missing from 'CreateSession' return value")} + return nil, &UnsupportedPlatformError{ + 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(sessionHandleString) + selectDevicesOptions := map[string]dbus.Variant{ + "types": dbus.MakeVariant(deviceKeyboard | devicePointer), } - 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 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} + return nil, &UnsupportedPlatformError{ + fmt.Errorf("calling 'SelectDevices' failed: %w", err), + } } - if result != 0 { - return nil, UnsupportedPlatformError{errors.New(fmt.Sprintf( - "Calling 'SelectDevices' failed (%v)", result))} - } - inVardict = make(map[string]dbus.Variant) - 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")} + 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, 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) @@ -161,47 +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 { - 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 + return p.bus.Close() } func (p *portalController) keyboardKeys(keys []Keysym) error { - p.lock.RLock() - defer p.lock.RUnlock() - if p.bus == nil { - return errors.New("dbus connection closed") - } - inVardict := make(map[string]dbus.Variant) for _, keysym := range keys { for _, 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 } } @@ -231,57 +358,42 @@ func (p *portalController) KeyboardKey(key Key) error { } func (p *portalController) PointerButton(button PointerButton, press bool) error { - p.lock.RLock() - defer p.lock.RUnlock() - if p.bus == nil { - return errors.New("dbus connection closed") - } var btn int32 - if button == PointerButtonLeft { + switch button { + case PointerButtonLeft: btn = btnLeft - } else if button == PointerButtonMiddle { + case PointerButtonMiddle: btn = btnMiddle - } else if button == PointerButtonRight { + case PointerButtonRight: btn = btnRight - } else { - return errors.New("unsupported pointer button") + default: + return fmt.Errorf("unsupported pointer button: %#v", 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 { + 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 { - 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 { + 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 { - 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 { + 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 diff --git a/inputcontrol/controller_uinput.go b/inputcontrol/controller_uinput.go new file mode 100644 index 0000000..2dd9bab --- /dev/null +++ b/inputcontrol/controller_uinput.go @@ -0,0 +1,200 @@ +//go:build uinput + +/* + * Copyright (c) 2023 De_Coder github.com/ps100000 + * Copyright (c) 2023 Unrud + * + * 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 . + */ + +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))) +} diff --git a/inputcontrol/controller_windows.go b/inputcontrol/controller_windows.go index eb844b2..58b5e9a 100644 --- a/inputcontrol/controller_windows.go +++ b/inputcontrol/controller_windows.go @@ -22,7 +22,7 @@ package inputcontrol import ( - "errors" + "fmt" "syscall" "unsafe" ) @@ -98,7 +98,7 @@ func init() { func InitWindowsController() (Controller, error) { p := &windowsController{} if err := sendInputProc.Find(); err != nil { - return nil, UnsupportedPlatformError{err} + return nil, &UnsupportedPlatformError{err} } return p, nil } @@ -111,9 +111,8 @@ func (p *windowsController) sendInput(inputs []keybdInput) error { if len(inputs) == 0 { return nil } - if r, _, err := sendInputProc.Call(uintptr(len(inputs)), - uintptr(unsafe.Pointer(&inputs[0])), - unsafe.Sizeof(inputs[0])); int(r) != len(inputs) { + if sent, _, err := sendInputProc.Call(uintptr(len(inputs)), + uintptr(unsafe.Pointer(&inputs[0])), unsafe.Sizeof(inputs[0])); int(sent) != len(inputs) { return err } return nil @@ -135,44 +134,45 @@ func (p *windowsController) KeyboardText(text string) error { func (p *windowsController) KeyboardKey(key Key) error { input := keybdInput{typ: inputKeyboard} - if key == KeyBackSpace { + switch key { + case KeyBackSpace: input.wVk = vkBack - } else if key == KeyReturn { + case KeyReturn: input.wVk = vkReturn - } else if key == KeyEnd { + case KeyEnd: input.wVk = vkEnd - } else if key == KeyHome { + case KeyHome: input.wVk = vkHome - } else if key == KeyLeft { + case KeyLeft: input.wVk = vkLeft - } else if key == KeyUp { + case KeyUp: input.wVk = vkUp - } else if key == KeyRight { + case KeyRight: input.wVk = vkRight - } else if key == KeyDown { + case KeyDown: input.wVk = vkDown - } else if key == KeyDelete { + case KeyDelete: input.wVk = vkDelete - } else if key == KeySuper { + case KeySuper: input.wVk = vkLwin - } else if key == KeyBrowserBack { + case KeyBrowserBack: input.wVk = vkBrowserBack - } else if key == KeyBrowserForward { + case KeyBrowserForward: input.wVk = vkBrowserForward - } else if key == KeyVolumeMute { + case KeyVolumeMute: input.wVk = vkVolumeMute - } else if key == KeyVolumeDown { + case KeyVolumeDown: input.wVk = vkVolumeDown - } else if key == KeyVolumeUp { + case KeyVolumeUp: input.wVk = vkVolumeUp - } else if key == KeyMediaNextTrack { + case KeyMediaNextTrack: input.wVk = vkMediaNextTrack - } else if key == KeyMediaPrevTrack { + case KeyMediaPrevTrack: input.wVk = vkMediaPrevTrack - } else if key == KeyMediaPlayPause { + case KeyMediaPlayPause: input.wVk = vkMediaPlayPause - } else { - return errors.New("key not mapped to virtual-key code") + default: + return fmt.Errorf("key not mapped to virtual-key code: %#v", key) } inputs := [...]keybdInput{input, input} inputs[1].dwFlags |= keyeventfKeyup @@ -194,10 +194,10 @@ func (p *windowsController) PointerButton(button PointerButton, press bool) erro } else if button == PointerButtonRight { input.dwFlags = mouseeventfRightup } 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)), - unsafe.Sizeof(input)); int(r) != 1 { + if sent, _, err := sendInputProc.Call(1, uintptr(unsafe.Pointer(&input)), + unsafe.Sizeof(input)); int(sent) != 1 { return err } return nil @@ -210,8 +210,8 @@ func (p *windowsController) PointerMove(deltaX, deltaY int) error { dy: int32(deltaY), dwFlags: mouseeventfMove, } - if r, _, err := sendInputProc.Call(1, uintptr(unsafe.Pointer(&input)), - unsafe.Sizeof(input)); int(r) != 1 { + if sent, _, err := sendInputProc.Call(1, uintptr(unsafe.Pointer(&input)), + unsafe.Sizeof(input)); int(sent) != 1 { return err } return nil @@ -236,9 +236,8 @@ func (p *windowsController) PointerScroll(deltaHorizontal, deltaVertical int, fi if len(inputs) == 0 { return nil } - if r, _, err := sendInputProc.Call(uintptr(len(inputs)), - uintptr(unsafe.Pointer(&inputs[0])), - unsafe.Sizeof(inputs[0])); int(r) != len(inputs) { + if sent, _, err := sendInputProc.Call(uintptr(len(inputs)), + uintptr(unsafe.Pointer(&inputs[0])), unsafe.Sizeof(inputs[0])); int(sent) != len(inputs) { return err } return nil diff --git a/inputcontrol/controller_x11.go b/inputcontrol/controller_x11.go index 3d7c9f7..f45b75b 100644 --- a/inputcontrol/controller_x11.go +++ b/inputcontrol/controller_x11.go @@ -21,28 +21,42 @@ package inputcontrol -// #cgo LDFLAGS: -lX11 -lXtst +// #cgo LDFLAGS: -lX11 -lXrandr -lXtst +// #include // #include // #include +// #include // #include // #include +// Window MacroDefaultRootWindow(Display *dpy) { +// return DefaultRootWindow(dpy); +// } import "C" + import ( "errors" "fmt" - "os" + "maps" + "slices" + "strings" "sync" "time" "unsafe" ) 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 @@ -55,17 +69,48 @@ func init() { } func InitX11Controller() (Controller, error) { - sessionType := os.Getenv("XDG_SESSION_TYPE") - if sessionType != "" && sessionType != "x11" { - return nil, UnsupportedPlatformError{errors.New(fmt.Sprintf( - "unsupported session type '%v'", sessionType))} - } display := C.XOpenDisplay(nil) if display == nil { - return nil, UnsupportedPlatformError{ - errors.New("failed to connect to X server")} + return nil, &UnsupportedPlatformError{ + errors.New("failed to connect to X server"), + } } - return &x11Controller{display: display}, nil + p := &x11Controller{display: display} + if p.xIsXwayland() { + p.Close() + return nil, &UnsupportedPlatformError{ + errors.New("X server is Xwayland"), + } + } + return p, nil +} + +func (p *x11Controller) xIsXwayland() bool { + // Detection method from https://gitlab.freedesktop.org/xorg/app/xisxwayland/-/blob/xisxwayland-2/xisxwayland.c + var opcode, event, error, major, minor C.int + xwaylandExtensionName := C.CString("XWAYLAND") + defer C.free(unsafe.Pointer(xwaylandExtensionName)) + if C.XQueryExtension(p.display, xwaylandExtensionName, &opcode, &event, &error) != 0 { + return true + } + if C.XRRQueryExtension(p.display, &event, &error) == 0 || + C.XRRQueryVersion(p.display, &major, &minor) == 0 { + return false + } + resources := C.XRRGetScreenResourcesCurrent(p.display, C.MacroDefaultRootWindow(p.display)) + if resources == nil { + return false + } + defer C.XRRFreeScreenResources(resources) + if resources.noutput < 1 { + return false + } + output := C.XRRGetOutputInfo(p.display, resources, *resources.outputs) + if output == nil { + return false + } + defer C.XRRFreeOutputInfo(output) + return strings.HasPrefix(C.GoString(output.name), "XWAYLAND") } func (p *x11Controller) Close() error { @@ -79,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< + * + * 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 . + */ + +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<> 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 +} diff --git a/inputcontrol/keysyms.go b/inputcontrol/keysyms.go index 5402120..de183a1 100644 --- a/inputcontrol/keysyms.go +++ b/inputcontrol/keysyms.go @@ -23,7 +23,7 @@ package inputcontrol //go:generate go run keysyms.generator.go -import "errors" +import "fmt" type Keysym int32 @@ -57,8 +57,8 @@ func RuneToKeysym(runeValue rune) (Keysym, error) { keysym, found := keysymsMap[runeValue] if !found { if runeValue < 0x100 || runeValue > 0x10ffff { - return 0, errors.New("rune not mappend to keysym and " + - "out of range for direct unicode mapping") + return 0, fmt.Errorf("rune not mappend to keysym and "+ + "out of range for direct unicode mapping: %q", runeValue) } keysym = Keysym(0x01000000 + runeValue) } @@ -66,59 +66,44 @@ func RuneToKeysym(runeValue rune) (Keysym, error) { } func KeyToKeysym(key Key) (Keysym, error) { - if key == KeyBackSpace { + switch key { + case KeyBackSpace: return xkBackSpace, nil - } - if key == KeyReturn { + case KeyReturn: return xkReturn, nil - } - if key == KeyDelete { + case KeyDelete: return xkDelete, nil - } - if key == KeyHome { + case KeyHome: return xkHome, nil - } - if key == KeyLeft { + case KeyLeft: return xkLeft, nil - } - if key == KeyUp { + case KeyUp: return xkUp, nil - } - if key == KeyRight { + case KeyRight: return xkRight, nil - } - if key == KeyDown { + case KeyDown: return xkDown, nil - } - if key == KeyEnd { + case KeyEnd: return xkEnd, nil - } - if key == KeySuper { + case KeySuper: return xkSuperL, nil - } - if key == KeyVolumeMute { + case KeyVolumeMute: return xf86xkAudioMute, nil - } - if key == KeyVolumeDown { + case KeyVolumeDown: return xf86xkAudioLowerVolume, nil - } - if key == KeyVolumeUp { + case KeyVolumeUp: return xf86xkAudioRaiseVolume, nil - } - if key == KeyMediaPlayPause { + case KeyMediaPlayPause: return xf86xkAudioPlay, nil - } - if key == KeyMediaPrevTrack { + case KeyMediaPrevTrack: return xf86xkAudioPrev, nil - } - if key == KeyMediaNextTrack { + case KeyMediaNextTrack: return xf86xkAudioNext, nil - } - if key == KeyBrowserBack { + case KeyBrowserBack: return xf86xkBack, nil - } - if key == KeyBrowserForward { + case KeyBrowserForward: return xf86xkForward, nil + default: + return 0, fmt.Errorf("key not mapped to keysym: %#v", key) } - return 0, errors.New("key not mapped to keysym") } diff --git a/main.go b/main.go index 5a7e7a0..f8db693 100644 --- a/main.go +++ b/main.go @@ -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.2.2" + version string = "1.5.4" prettyAppName string = "Remote Touchpad" ) @@ -184,21 +185,25 @@ func main() { } var controller inputcontrol.Controller var controllerName string - platformErrors := "" + var platformErrs []error for _, controllerInfo := range inputcontrol.Controllers { controllerName = controllerInfo.Name var err error controller, err = controllerInfo.Init() if err == nil { break - } else if _, ok := err.(inputcontrol.UnsupportedPlatformError); ok { - platformErrors += fmt.Sprintf("%s controller: %v\n", controllerName, err) } else { - log.Fatalf("%s controller: %v", controllerName, err) + var unsupportedErr *inputcontrol.UnsupportedPlatformError + wrappedErr := fmt.Errorf("%v controller: %w", controllerName, err) + if errors.As(err, &unsupportedErr) { + platformErrs = append(platformErrs, wrappedErr) + } else { + log.Fatal(wrappedErr) + } } } if controller == nil { - log.Fatal("unsupported platform:\n" + platformErrors) + log.Fatal(fmt.Errorf("unsupported platform:\n%w", errors.Join(platformErrs...))) } defer controller.Close() authenticationChallenges := make(chan challenge, authenticationRateBurst) @@ -241,7 +246,7 @@ func main() { return } if err := processCommand(controller, message); err != nil { - log.Printf("%s controller: %v", controllerName, err) + log.Print(fmt.Errorf("%s controller: %w", controllerName, err)) return } } diff --git a/remote-touchpad.service b/remote-touchpad.service new file mode 100644 index 0000000..76f5a74 --- /dev/null +++ b/remote-touchpad.service @@ -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 \ No newline at end of file diff --git a/rts b/rts new file mode 100755 index 0000000..84fbe46 --- /dev/null +++ b/rts @@ -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 diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index f32c543..6d23326 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -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: core18 +base: core24 confinement: strict +platforms: + amd64: + arm64: + apps: remote-touchpad: command: bin/remote-touchpad @@ -20,13 +24,13 @@ apps: - desktop - x11 remote-touchpad-wait-on-error: - command: usr/bin/remote-touchpad-wait-on-error + command: bin/remote-touchpad-wait-on-error plugs: - network-bind - 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,18 +40,22 @@ parts: - x11 source: . source-type: git + build-snaps: + - go build-packages: - gcc - libc6-dev - libxt-dev + - libxrandr-dev - libxtst-dev stage-packages: - libxt6 + - libxrandr2 - libxtst6 override-build: | - snapcraftctl build - install -Dm0755 -t "${SNAPCRAFT_PART_INSTALL}/usr/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] diff --git a/webdata.go b/webdata.go index 4d07131..1b816b6 100644 --- a/webdata.go +++ b/webdata.go @@ -1,19 +1,52 @@ +/* + * Copyright (c) 2024 Unrud + * + * 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 . + */ + 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) + } + } } diff --git a/webdata/app/compat.mjs b/webdata/app/compat.mjs new file mode 100644 index 0000000..8a26833 --- /dev/null +++ b/webdata/app/compat.mjs @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2018-2019, 2023 Unrud + * + * 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 . + */ + +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); + } +}; diff --git a/webdata/app/inputcontroller.mjs b/webdata/app/inputcontroller.mjs new file mode 100644 index 0000000..a2984fb --- /dev/null +++ b/webdata/app/inputcontroller.mjs @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2018-2019, 2023 Unrud + * + * 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 . + */ + +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); + } +} diff --git a/webdata/app/keyboard.mjs b/webdata/app/keyboard.mjs new file mode 100644 index 0000000..84195d9 --- /dev/null +++ b/webdata/app/keyboard.mjs @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2018-2019, 2023 Unrud + * + * 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 . + */ + +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); + } + } +} diff --git a/webdata/app/main.mjs b/webdata/app/main.mjs new file mode 100644 index 0000000..5477b89 --- /dev/null +++ b/webdata/app/main.mjs @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2018-2019, 2023 Unrud + * + * 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 . + */ + +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]; + } +} diff --git a/webdata/app/mouse.mjs b/webdata/app/mouse.mjs new file mode 100644 index 0000000..8158eb3 --- /dev/null +++ b/webdata/app/mouse.mjs @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2018-2019, 2023 Unrud + * + * 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 . + */ + +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); + } + } +} diff --git a/webdata/app/sha256.mjs b/webdata/app/sha256.mjs new file mode 100644 index 0000000..3f7cd3f --- /dev/null +++ b/webdata/app/sha256.mjs @@ -0,0 +1,9 @@ +/** + * A JavaScript implementation of the SHA family of hashes - defined in FIPS PUB 180-4, FIPS PUB 202, + * and SP 800-185 - as well as the corresponding HMAC implementation as defined in FIPS PUB 198-1. + * + * Copyright 2008-2022 Brian Turek, 1998-2009 Paul Johnston & Contributors + * Distributed under the BSD License + * See http://caligatio.github.com/jsSHA/ for more information + */ +const t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";function r(t,r,n,i){let e,s,o;const h=r||[0],u=(n=n||0)>>>3,f=-1===i?3:0;for(e=0;e>>2,h.length<=s&&h.push(0),h[s]|=t[e]<<8*(f+i*(o%4));return{value:h,binLen:8*t.length+n}}function n(n,i,e){switch(i){case"UTF8":case"UTF16BE":case"UTF16LE":break;default:throw new Error("encoding must be UTF8, UTF16BE, or UTF16LE")}switch(n){case"HEX":return function(t,r,n){return function(t,r,n,i){let e,s,o,h;if(0!=t.length%2)throw new Error("String of HEX type must be in byte increments");const u=r||[0],f=(n=n||0)>>>3,c=-1===i?3:0;for(e=0;e>>1)+f,o=h>>>2;u.length<=o;)u.push(0);u[o]|=s<<8*(c+i*(h%4))}return{value:u,binLen:4*t.length+n}}(t,r,n,e)};case"TEXT":return function(t,r,n){return function(t,r,n,i,e){let s,o,h,u,f,c,a,w,A=0;const E=n||[0],l=(i=i||0)>>>3;if("UTF8"===r)for(a=-1===e?3:0,h=0;hs?o.push(s):2048>s?(o.push(192|s>>>6),o.push(128|63&s)):55296>s||57344<=s?o.push(224|s>>>12,128|s>>>6&63,128|63&s):(h+=1,s=65536+((1023&s)<<10|1023&t.charCodeAt(h)),o.push(240|s>>>18,128|s>>>12&63,128|s>>>6&63,128|63&s)),u=0;u>>2;E.length<=f;)E.push(0);E[f]|=o[u]<<8*(a+e*(c%4)),A+=1}else for(a=-1===e?2:0,w="UTF16LE"===r&&1!==e||"UTF16LE"!==r&&1===e,h=0;h>>8),c=A+l,f=c>>>2;E.length<=f;)E.push(0);E[f]|=s<<8*(a+e*(c%4)),A+=2}return{value:E,binLen:8*A+i}}(t,i,r,n,e)};case"B64":return function(r,n,i){return function(r,n,i,e){let s,o,h,u,f,c,a,w=0;const A=n||[0],E=(i=i||0)>>>3,l=-1===e?3:0,p=r.indexOf("=");if(-1===r.search(/^[a-zA-Z0-9=+/]+$/))throw new Error("Invalid character in base-64 string");if(r=r.replace(/=/g,""),-1!==p&&p 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); + } + } +} diff --git a/webdata/fn.js b/webdata/fn.js deleted file mode 100644 index bf1d1ce..0000000 --- a/webdata/fn.js +++ /dev/null @@ -1,696 +0,0 @@ -"use strict"; -/* - * Copyright (c) 2018-2019 Unrud - * - * 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 . - */ - -// [1 Touch, 2 Touches, 3 Touches] (as pixel) -var TOUCH_MOVE_THRESHOLD = [10, 15, 15]; -// Max time between consecutive touches for clicking or dragging (as milliseconds) -var TOUCH_TIMEOUT = 250; -// [[pixel/second, multiplicator], ...] -var POINTER_ACCELERATION = [ - [0, 0], - [87, 1], - [173, 1], - [553, 2] -]; - -var POINTER_BUTTON_LEFT = 0; -var POINTER_BUTTON_RIGHT = 1; -var POINTER_BUTTON_MIDDLE = 2; - -var KEY_VOLUME_MUTE = 0; -var KEY_VOLUME_DOWN = 1; -var KEY_VOLUME_UP = 2; -var KEY_MEDIA_PLAY_PAUSE = 3; -var KEY_MEDIA_PREV_TRACK = 4; -var KEY_MEDIA_NEXT_TRACK = 5; -var KEY_BROWSER_BACK = 6; -var KEY_BROWSER_FORWARD = 7; -var KEY_SUPER = 8; -var KEY_LEFT = 9; -var KEY_RIGHT = 10; -var KEY_UP = 11; -var KEY_DOWN = 12; -var KEY_HOME = 13; -var KEY_END = 14; -var KEY_BACK_SPACE = 15; -var KEY_DELETE = 16; -var KEY_RETURN = 17; - -var ws = null; -var config = null; - -var util = (function() { - var util = {}; - - util.fullscreenEnabled = function() { - return (document.fullscreenEnabled || - document.webkitFullscreenEnabled || - document.mozFullScreenEnabled || - document.msFullscreenEnabled || - false); - }; - - util.requestFullscreen = function(element, options) { - if (element.requestFullscreen) { - element.requestFullscreen(options); - } else if (element.webkitRequestFullscreen) { - element.webkitRequestFullscreen(options); - } else if (element.mozRequestFullScreen) { - element.mozRequestFullScreen(options); - } else if (element.msRequestFullscreen) { - element.msRequestFullscreen(options); - } - }; - - util.exitFullscreen = function() { - if (document.exitFullscreen) { - document.exitFullscreen(); - } else if (document.webkitExitFullscreen) { - document.webkitExitFullscreen(); - } else if (document.mozCancelFullScreen) { - document.mozCancelFullScreen(); - } else if (document.msExitFullscreen) { - document.msExitFullscreen(); - } - }; - - util.fullscreenElement = function() { - return (document.fullscreenElement || - document.webkitFullscreenElement || - document.mozFullScreenElement || - document.msFullscreenElement || - null); - }; - - util.addFullscreenchangeEventListener = function(listener) { - if ("onfullscreenchange" in document) { - document.addEventListener("fullscreenchange", listener); - } else if ("onwebkitfullscreenchange" in document) { - document.addEventListener("webkitfullscreenchange", listener); - } - }; - - util.requestPointerLock = function(element) { - if (element.requestPointerLock) { - element.requestPointerLock(); - } else if (element.mozRequestPointerLock) { - element.mozRequestPointerLock(); - } - }; - - util.exitPointerLock = function() { - if (document.exitPointerLock) { - document.exitPointerLock(); - } else if (document.mozExitPointerLock) { - document.mozExitPointerLock(); - } - }; - - util.pointerLockElement = function() { - return (document.pointerLockElement || - document.mozPointerLockElement || - null); - }; - - util.addPointerlockchangeEventListener = function(listener) { - if ("onpointerlockchange" in document) { - document.addEventListener("pointerlockchange", listener); - } else if ("onmozpointerlockchange" in document) { - document.addEventListener("mozpointerlockchange", listener); - } - }; - - return util; -})(); - -var controller = (function() { - var controller = {}; - - var moveXSum = 0; - var moveYSum = 0; - var scrollHSum = 0; - var scrollVSum = 0; - var scrolling = false; - var scrollFinish = false; - var updateTimoueActive = false; - - function startUpdate(fromTimeout) { - if (updateTimoueActive && !fromTimeout) { - return; - } - updateTimoueActive = false; - var finished = true; - var xInt = Math.trunc(moveXSum); - var yInt = Math.trunc(moveYSum); - if (xInt != 0 || yInt != 0) { - ws.send("m" + xInt + ";" + yInt); - moveXSum -= xInt; - moveYSum -= yInt; - finished = false; - } - var hInt = Math.trunc(scrollHSum); - var vInt = Math.trunc(scrollVSum); - if (hInt != 0 || vInt != 0) { - ws.send((scrollFinish ? "S" : "s") + hInt + ";" + vInt); - scrollHSum -= hInt; - scrollVSum -= vInt; - scrolling = !scrollFinish; - scrollFinish = false; - finished = false; - } else if (scrollFinish && scrolling) { - ws.send("S"); - scrolling = false; - scrollFinish = false; - } - updateTimoueActive = !finished && config.updateRate > 0; - if (updateTimoueActive) { - setTimeout(startUpdate, 1000/config.updateRate, true); - } - } - - controller.pointerMove = function(deltaX, deltaY) { - moveXSum += deltaX; - moveYSum += deltaY; - startUpdate(); - }; - - controller.pointerScroll = function(deltaHorizontal, deltaVertical, finish) { - scrollHSum += deltaHorizontal; - scrollVSum += deltaVertical; - scrollFinish |= finish; - startUpdate(); - }; - - controller.pointerButton = function(button, press) { - ws.send("b" + button + ";" + (press ? 1 : 0)); - }; - - - controller.keyboardKey = function(key) { - ws.send("k" + key); - }; - - controller.keyboardText = function(text) { - ws.send("t" + text); - }; - - return controller; -})(); - -var touchpad = (function() { - var touchpad = {}; - - var moved = false; - var startTimeStamp = 0; - var lastEndTimeStamp = 0; - var releasedCount = 0; - var ongoingTouches = []; - var dragging = false; - var draggingTimeout = null; - - function copyTouch(touch, timeStamp) { - return { - identifier: touch.identifier, - pageX: touch.pageX, - pageXStart: touch.pageX, - pageY: touch.pageY, - pageYStart: touch.pageY, - timeStamp: timeStamp - }; - } - - function ongoingTouchIndexById(idToFind) { - for (var i = 0; i < ongoingTouches.length; i += 1) { - if (ongoingTouches[i].identifier == idToFind) { - return i; - } - } - return -1; - } - - function calculateAccelerationMult(speed) { - for (var i = 0; i < POINTER_ACCELERATION.length; i += 1) { - var s2 = POINTER_ACCELERATION[i][0]; - var a2 = POINTER_ACCELERATION[i][1]; - if (s2 <= speed) { - continue; - } - if (i == 0) { - return a2; - } - var s1 = POINTER_ACCELERATION[i - 1][0]; - var a1 = POINTER_ACCELERATION[i - 1][1]; - return ((speed - s1) / (s2 - s1)) * (a2 - a1) + a1; - } - if (POINTER_ACCELERATION.length > 0) { - return POINTER_ACCELERATION[POINTER_ACCELERATION.length - 1][1]; - } - return 1; - } - - function onDraggingTimeout() { - draggingTimeout = null; - controller.pointerButton(POINTER_BUTTON_LEFT, false); - } - - touchpad.handleTouchstart = function(evt) { - // Might get called multiple times for the same touches - if (ongoingTouches.length == 0) { - startTimeStamp = evt.timeStamp; - moved = false; - } - var touches = evt.changedTouches; - var foundTouch = false; - for (var i = 0; i < touches.length; i += 1) { - if (ongoingTouches.length == 0 && !touches[i].target.classList.contains("touch")) { - continue; - } - foundTouch = true; - var touch = copyTouch(touches[i], evt.timeStamp); - var idx = ongoingTouchIndexById(touch.identifier); - if (idx < 0) { - ongoingTouches.push(touch); - } else { - ongoingTouches[idx] = touch; - } - } - if (!foundTouch) { - return; - } - evt.preventDefault(); - lastEndTimeStamp = 0; - if (draggingTimeout != null) { - clearTimeout(draggingTimeout); - draggingTimeout = null; - dragging = true; - } - controller.pointerScroll(0, 0, true); - }; - - touchpad.handleTouchend = touchpad.handleTouchcancel = function(evt) { - var touches = evt.changedTouches; - var foundTouch = false; - for (var i = 0; i < touches.length; i += 1) { - var idx = ongoingTouchIndexById(touches[i].identifier); - if (idx < 0) { - continue; - } - foundTouch = true; - ongoingTouches.splice(idx, 1); - releasedCount += 1; - } - if (!foundTouch) { - return; - } - evt.preventDefault(); - lastEndTimeStamp = evt.timeStamp; - controller.pointerScroll(0, 0, true); - if (releasedCount > TOUCH_MOVE_THRESHOLD.length) { - moved = true; - } - if (ongoingTouches.length == 0 && releasedCount >= 1) { - if (dragging) { - dragging = false; - controller.pointerButton(POINTER_BUTTON_LEFT, false); - } - if (!moved && evt.timeStamp - startTimeStamp < TOUCH_TIMEOUT) { - var button = 0; - if (releasedCount == 1) { - button = POINTER_BUTTON_LEFT; - } else if (releasedCount == 2) { - button = POINTER_BUTTON_RIGHT; - } else if (releasedCount == 3) { - button = POINTER_BUTTON_MIDDLE; - } - controller.pointerButton(button, true); - if (button == POINTER_BUTTON_LEFT) { - draggingTimeout = setTimeout(onDraggingTimeout, TOUCH_TIMEOUT); - } else { - controller.pointerButton(button, false); - } - } - releasedCount = 0; - } - }; - - touchpad.handleTouchmove = function(evt) { - var sumX = 0; - var sumY = 0; - var touches = evt.changedTouches; - var foundTouch = false; - for (var i = 0; i < touches.length; i += 1) { - var idx = ongoingTouchIndexById(touches[i].identifier); - if (idx < 0) { - continue; - } - foundTouch = true; - if (!moved) { - var dist = Math.sqrt(Math.pow(touches[i].pageX - ongoingTouches[idx].pageXStart, 2) + - Math.pow(touches[i].pageY - ongoingTouches[idx].pageYStart, 2)); - if (ongoingTouches.length > TOUCH_MOVE_THRESHOLD.length || - dist > TOUCH_MOVE_THRESHOLD[ongoingTouches.length - 1] || - evt.timeStamp - startTimeStamp >= TOUCH_TIMEOUT) { - moved = true; - } - } - var dx = touches[i].pageX - ongoingTouches[idx].pageX; - var dy = touches[i].pageY - ongoingTouches[idx].pageY; - var timeDelta = evt.timeStamp - ongoingTouches[idx].timeStamp; - sumX += dx * calculateAccelerationMult(Math.abs(dx) / timeDelta * 1000); - sumY += dy * calculateAccelerationMult(Math.abs(dy) / timeDelta * 1000); - ongoingTouches[idx].pageX = touches[i].pageX; - ongoingTouches[idx].pageY = touches[i].pageY; - ongoingTouches[idx].timeStamp = evt.timeStamp; - } - if (!foundTouch) { - return; - } - evt.preventDefault(); - if (moved && evt.timeStamp - lastEndTimeStamp >= TOUCH_TIMEOUT) { - if (ongoingTouches.length == 1 || dragging) { - controller.pointerMove(sumX*config.moveSpeed, sumY*config.moveSpeed); - } else if (ongoingTouches.length == 2) { - controller.pointerScroll(-sumX*config.scrollSpeed, -sumY*config.scrollSpeed, false); - } - } - }; - - return touchpad; -})(); - -var keyboard = (function() { - var keyboard = {}; - - keyboard.handleKeydown = function(evt) { - if (evt.ctrlKey || evt.altKey || evt.isComposing) { - return; - } - var key = null; - if (evt.key == "OS" || evt.key == "Super" || evt.key == "Meta") { - key = KEY_SUPER; - } else if (evt.key == "Backspace") { - key = KEY_BACK_SPACE; - } else if (evt.key == "Enter") { - key = KEY_RETURN; - } else if (evt.key == "Delete") { - key = KEY_DELETE; - } else if (evt.key == "Home") { - key = KEY_HOME; - } else if (evt.key == "End") { - key = KEY_END; - } else if (evt.key == "Left" || evt.key == "ArrowLeft") { - key = KEY_LEFT; - } else if (evt.key == "Right" || evt.key == "ArrowRight") { - key = KEY_RIGHT; - } else if (evt.key == "Up" || evt.key == "ArrowUp") { - key = KEY_UP; - } else if (evt.key == "Down" || evt.key == "ArrowDown") { - key = KEY_DOWN; - } - if (key != null) { - if (!evt.shiftKey) { - evt.preventDefault(); - controller.keyboardKey(key); - } - } else if (evt.key.length == 1) { - evt.preventDefault(); - controller.keyboardText(evt.key); - } - }; - - return keyboard; -})(); - -var mouse = (function() { - var mouse = {}; - - var buttons = 0; - - function updateButtons(newButtons) { - for (var button = 0; button < 3; button += 1) { - var flag = 1 << button; - if ((newButtons&flag) != (buttons&flag)) { - controller.pointerButton(button, newButtons&flag); - } - } - buttons = newButtons; - } - - mouse.handleMousedown = mouse.handleMouseup = function(evt) { - updateButtons(evt.buttons); - }; - - mouse.handleMousemove = function(evt) { - controller.pointerMove(evt.movementX*config.mouseMoveSpeed, evt.movementY*config.mouseMoveSpeed); - }; - - mouse.handleWheel = function(evt) { - if (evt.deltaMode == WheelEvent.DOM_DELTA_PIXEL) { - controller.pointerScroll(evt.deltaX*config.mouseScrollSpeed, evt.deltaY*config.mouseScrollSpeed, true); - } else if (evt.deltaMode == WheelEvent.DOM_DELTA_LINE) { - controller.pointerScroll(evt.deltaX*20*config.mouseScrollSpeed, evt.deltaY*20*config.mouseScrollSpeed, true); - } - }; - - return mouse; -})(); - -function challengeResponse(message) { - var shaObj = new jsSHA("SHA-256", "TEXT"); - shaObj.setHMACKey(message, "TEXT"); - shaObj.update(window.location.hash.substr(1)); - return btoa(shaObj.getHMAC("BYTES")); -} - -window.addEventListener("load", function() { - var DEFAULT_KEYS_SCENE = { - "": 0, - "keyboard": 1 - }; - var ready = false; - var closed = false; - var scenes = document.querySelectorAll("body > .scene"); - var openingScene = document.getElementById("opening"); - var closedScene = document.getElementById("closed"); - var padScene = document.getElementById("pad"); - var keysScene = document.getElementById("keys"); - var keysSubScenes = keysScene.querySelectorAll(".scene"); - var keyboardScene = document.getElementById("keyboard"); - var fullscreenbutton = document.getElementById("fullscreenbutton"); - var keyboardTextarea = keyboardScene.querySelector("textarea"); - var mouseScene = document.getElementById("mouse"); - var activeScene = null; - var keysActiveName = ""; - - function showScene(scene) { - activeScene = scene; - if (util.fullscreenElement() && !scene.classList.contains("fullscreen")) { - util.exitFullscreen(); - } - if (util.pointerLockElement() && activeScene != mouseScene) { - util.exitPointerLock(); - } - keyboardTextarea.value = ""; - scenes.forEach(function(otherScene) { - otherScene.classList.toggle("hidden", otherScene != scene); - }); - } - - function showKeysScene(index) { - if (!Number.isInteger(index) || index < 0 || keysSubScenes.length <= index) { - index = 0; - } - sessionStorage.setItem(keysActiveName, index); - for (var i = 0; i < keysSubScenes.length; i += 1) { - keysSubScenes[i].classList.toggle("hidden", i != index); - } - } - - function showKeys(name) { - showScene(keysScene); - keysActiveName = "keys" + (name ? ":" + name : ""); - var keysIndex = parseInt(sessionStorage.getItem(keysActiveName)); - if (isNaN(keysIndex)) { - keysIndex = DEFAULT_KEYS_SCENE[name || ""] || 0; - } - showKeysScene(keysIndex); - if (history.state != keysActiveName) { - history.pushState(keysActiveName, ""); - } - } - - function showKeyboard() { - showScene(keyboardScene); - keyboardTextarea.value = sessionStorage.getItem("keyboard") || ""; - keyboardTextarea.focus(); - if (history.state != "keyboard") { - history.pushState("keyboard", ""); - } - } - - keyboardTextarea.oninput = function() { - sessionStorage.setItem("keyboard", keyboardTextarea.value); - }; - - function updateUI() { - if (!ready) { - showScene(closed ? closedScene : openingScene); - } else if (util.pointerLockElement()) { - showScene(mouseScene); - } else if ((history.state || "").split(":")[0] == "keys") { - showKeys(history.state.substr("keys:".length)); - } else if (history.state == "keyboard") { - showKeyboard(); - } else { - showScene(padScene); - } - } - - updateUI(); - - var wsURL = new URL("ws", location.href); - wsURL.protocol = wsURL.protocol == "http:" ? "ws:" : "wss:"; - ws = new WebSocket(wsURL); - - var authenticated = false; - ws.onmessage = function(evt) { - if (!authenticated) { - ws.send(challengeResponse(evt.data)); - authenticated = true; - return; - } - try { - config = JSON.parse(evt.data); - } catch (e) { - ws.close(); - throw (e); - } - ready = true; - updateUI(); - }; - - ws.onclose = function() { - ready = false; - closed = true; - updateUI(); - }; - - document.getElementById("keysbutton").addEventListener("click", function() { - showKeys(); - }); - document.getElementById("keyboardbutton").addEventListener("click", function() { - showKeyboard(); - }); - util.addFullscreenchangeEventListener(function() { - updateUI(); - }); - if (!util.fullscreenEnabled()) { - fullscreenbutton.classList.add("hidden"); - } - fullscreenbutton.addEventListener("click", function() { - if (util.fullscreenElement()) { - util.exitFullscreen(); - } else { - util.requestFullscreen(document.documentElement, {navigationUI: "hide"}); - } - }); - document.getElementById("switchbutton").addEventListener("click", function() { - var keysIndex = 0; - for (var i = 0; i < keysSubScenes.length; i += 1) { - if (!keysSubScenes[i].classList.contains("hidden")) { - keysIndex = i; - } - } - showKeysScene(keysIndex + 1); - }); - [ - {id: "browserbackbutton", key: KEY_BROWSER_BACK}, - {id: "superbutton", key: KEY_SUPER}, - {id: "browserforwardbutton", key: KEY_BROWSER_FORWARD}, - {id: "prevtrackbutton", key: KEY_MEDIA_PREV_TRACK}, - {id: "playpausebutton", key: KEY_MEDIA_PLAY_PAUSE}, - {id: "nexttrackbutton", key: KEY_MEDIA_NEXT_TRACK}, - {id: "volumedownbutton", key: KEY_VOLUME_DOWN}, - {id: "volumemutebutton", key: KEY_VOLUME_MUTE}, - {id: "volumeupbutton", key: KEY_VOLUME_UP}, - {id: "backspacebutton", key: KEY_BACK_SPACE}, - {id: "returnbutton", key: KEY_RETURN}, - {id: "deletebutton", key: KEY_DELETE}, - {id: "homebutton", key: KEY_HOME}, - {id: "endbutton", key: KEY_END}, - {id: "leftbutton", key: KEY_LEFT}, - {id: "rightbutton", key: KEY_RIGHT}, - {id: "upbutton", key: KEY_UP}, - {id: "downbutton", key: KEY_DOWN} - ].forEach(function(o) { - document.getElementById(o.id).addEventListener("click", function() { - controller.keyboardKey(o.key); - }); - }); - document.getElementById("keyboardkeysbutton").addEventListener("click", function() { - showKeys("keyboard"); - }); - document.getElementById("sendbutton").addEventListener("click", function() { - if (keyboardTextarea.value) { - // normalize line endings - controller.keyboardText(keyboardTextarea.value.replace(/\r\n?/g, "\n")); - keyboardTextarea.value = ""; - keyboardTextarea.oninput(); - } - history.back(); - }); - window.addEventListener("popstate", function() { - updateUI(); - }); - document.getElementById("reloadbutton").addEventListener("click", function() { - location.reload(); - }); - document.querySelectorAll(".backbutton").forEach(function(button) { - button.addEventListener("click", function() { - history.back(); - }); - }); - document.addEventListener("touchstart", touchpad.handleTouchstart); - document.addEventListener("touchend", touchpad.handleTouchend); - document.addEventListener("touchcancel", touchpad.handleTouchcancel); - document.addEventListener("touchmove", touchpad.handleTouchmove); - document.addEventListener("keydown", function(evt) { - if (activeScene && activeScene.classList.contains("key")) { - keyboard.handleKeydown(evt); - } - }); - util.addPointerlockchangeEventListener(function() { - updateUI(); - }); - document.addEventListener("mousedown", function(event) { - if (activeScene != mouseScene && event.buttons == 1 && event.target.classList.contains("touch")) { - util.requestPointerLock(mouseScene); - } - }); - ["touchstart", "touchend", "touchcancel", "touchmove"].forEach(function(type) { - mouseScene.addEventListener(type, function(evt) { - evt.preventDefault(); - }); - }); - mouseScene.addEventListener("mousedown", mouse.handleMousedown); - mouseScene.addEventListener("mouseup", mouse.handleMouseup); - mouseScene.addEventListener("mousemove", mouse.handleMousemove); - mouseScene.addEventListener("wheel", mouse.handleWheel); -}); diff --git a/webdata/index.html b/webdata/index.html index ac1968d..52fb46f 100644 --- a/webdata/index.html +++ b/webdata/index.html @@ -1,10 +1,8 @@ - - - - + + Remote Touchpad @@ -13,61 +11,90 @@

    Connecting…

    -