diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml deleted file mode 100644 index 9f3768a..0000000 --- a/.github/workflows/test.yaml +++ /dev/null @@ -1,43 +0,0 @@ -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 deleted file mode 100644 index 07c477a..0000000 --- a/.gitignore +++ /dev/null @@ -1,11 +0,0 @@ -# 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 e59454c..d42f03f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,78 +1,5 @@ # 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 e039950..9cd73c2 100644 --- a/README.md +++ b/README.md @@ -12,45 +12,8 @@ Supports Flatpak's RemoteDesktop portal (for Wayland), Windows and X11. * [Snap](https://snapcraft.io/remote-touchpad) * [Windows](https://github.com/Unrud/remote-touchpad/releases/latest) * Golang: - * Portal & uinput & X11: - - ```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. + * Portal & X11: `go install -tags portal,x11 github.com/unrud/remote-touchpad@latest` + * Windows: `go install github.com/unrud/remote-touchpad@latest` ## Screenshots diff --git a/desktop/com.github.unrud.RemoteTouchpad.metainfo.xml b/desktop/com.github.unrud.RemoteTouchpad.metainfo.xml index 113ef4d..87ba074 100644 --- a/desktop/com.github.unrud.RemoteTouchpad.metainfo.xml +++ b/desktop/com.github.unrud.RemoteTouchpad.metainfo.xml @@ -17,131 +17,8 @@ com.github.unrud.RemoteTouchpad.desktop https://github.com/Unrud/remote-touchpad/issues https://github.com/Unrud/remote-touchpad - https://github.com/Unrud/remote-touchpad - Unrud - - 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
  • -
-
-
    @@ -373,16 +250,16 @@ - https://raw.githubusercontent.com/Unrud/remote-touchpad/v1.5.4/screenshots/1.png + https://raw.githubusercontent.com/Unrud/remote-touchpad/v1.2.2/screenshots/1.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/2.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/3.png - https://raw.githubusercontent.com/Unrud/remote-touchpad/v1.5.4/screenshots/4.png + https://raw.githubusercontent.com/Unrud/remote-touchpad/v1.2.2/screenshots/4.png diff --git a/flatpak/com.github.unrud.RemoteTouchpad.yaml b/flatpak/com.github.unrud.RemoteTouchpad.yaml index 239160e..8fecb8d 100644 --- a/flatpak/com.github.unrud.RemoteTouchpad.yaml +++ b/flatpak/com.github.unrud.RemoteTouchpad.yaml @@ -1,24 +1,26 @@ app-id: com.github.unrud.RemoteTouchpad runtime: org.freedesktop.Platform -runtime-version: '25.08' +runtime-version: '22.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.2.2 - commit: a8ac15ba63645f02ffd57f4b443203279ab40b30 + tag: v5.1.0 + commit: e523abc905595cf17fb0001a7d77eaaddfaa216d dest: vendor/github.com/godbus/dbus/v5 - type: git url: https://github.com/skip2/go-qrcode @@ -26,17 +28,8 @@ modules: dest: vendor/github.com/skip2/go-qrcode - type: git url: https://github.com/golang/net - tag: v0.57.0 - commit: b8f09f6f062ceb4531b7af4bd17a5c8fe9c4b2b5 + commit: 8be639271d50c5f812a860bf5b9c21d50f14e8d1 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 deleted file mode 100644 index 6092904..0000000 --- a/flatpak/modules.txt +++ /dev/null @@ -1,17 +0,0 @@ -# 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 5afd19d..d774cf9 100644 --- a/go.mod +++ b/go.mod @@ -1,12 +1,9 @@ module github.com/unrud/remote-touchpad -go 1.25.5 +go 1.17 require ( - github.com/bendahl/uinput v1.7.0 - github.com/godbus/dbus/v5 v5.2.2 + github.com/godbus/dbus/v5 v5.1.0 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e - golang.org/x/net v0.57.0 + golang.org/x/net v0.0.0-20220923203811-8be639271d50 ) - -require golang.org/x/sys v0.47.0 // indirect diff --git a/go.sum b/go.sum index 783b4df..792c79a 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,11 @@ -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/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 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.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= +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= diff --git a/host.go b/host.go index 622b3bf..54874e5 100644 --- a/host.go +++ b/host.go @@ -74,8 +74,7 @@ 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 111e937..7e9e768 100644 --- a/inputcontrol/controller.go +++ b/inputcontrol/controller.go @@ -21,10 +21,8 @@ package inputcontrol import "sort" -type ( - PointerButton int - Key int -) +type PointerButton int +type Key int const ( PointerButtonLeft PointerButton = iota @@ -72,15 +70,11 @@ 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) Unwrap() error { - return e.Err +func (e UnsupportedPlatformError) Error() string { + return e.err.Error() } type Controller interface { diff --git a/inputcontrol/controller_null.go b/inputcontrol/controller_null.go deleted file mode 100644 index 6188cce..0000000 --- a/inputcontrol/controller_null.go +++ /dev/null @@ -1,66 +0,0 @@ -//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 99ecf8b..a9e503f 100644 --- a/inputcontrol/controller_portal.go +++ b/inputcontrol/controller_portal.go @@ -22,20 +22,10 @@ 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 ( @@ -45,8 +35,6 @@ const ( btnReleased uint32 = 0 btnPressed uint32 = 1 - untilRevoked uint32 = 2 - // linux/input-event-codes.h btnLeft int32 = 0x110 btnRight int32 = 0x111 @@ -55,8 +43,9 @@ const ( type portalController struct { bus *dbus.Conn - portalDesktop dbus.BusObject + remoteDesktop dbus.BusObject sessionHandle dbus.ObjectPath + lock sync.RWMutex } func init() { @@ -66,7 +55,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() { @@ -76,215 +65,91 @@ 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} } - portalDesktop := bus.Object("org.freedesktop.portal.Desktop", + remoteDesktop := bus.Object("org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop") - remoteDesktopVersionV, err := portalDesktop.GetProperty( - "org.freedesktop.portal.RemoteDesktop.version") - if err != nil { - return nil, &UnsupportedPlatformError{ - fmt.Errorf("getting 'version' failed: %w", err), - } - } - remoteDesktopVersion, ok := remoteDesktopVersionV.Value().(uint32) - if !ok { - return nil, &UnsupportedPlatformError{ - errors.New("unexpected 'version' type"), - } - } - restoreTokenStore, err := func() (*secretStore, error) { - if remoteDesktopVersion < 2 { - return nil, nil - } - cacheDirectory, err := os.UserCacheDir() - if err != nil { - return nil, err - } - if err := os.MkdirAll(cacheDirectory, 0o700); err != nil { - return nil, err - } - secret, err := retrieveSecret(bus) - if err != nil { - return nil, err - } - return newSecretStore(secret, - filepath.Join(cacheDirectory, "remote-touchpad.portal-restore-token.bin")) - }() - if err != nil { - log.Printf("Skipping restore token: %v", err) - } - availableDeviceTypesV, err := portalDesktop.GetProperty( + availableDeviceTypesV, err := remoteDesktop.GetProperty( "org.freedesktop.portal.RemoteDesktop.AvailableDeviceTypes") if err != nil { - return nil, &UnsupportedPlatformError{ - fmt.Errorf("getting 'AvailableDeviceTypes' failed: %w", err), - } + return nil, UnsupportedPlatformError{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")} } - createSessionResults, err := checkResponse(getResponse(bus, portalDesktop, - "org.freedesktop.portal.RemoteDesktop.CreateSession", 0, - map[string]dbus.Variant{"session_handle_token": dbus.MakeVariant("t")}, - )) + inVardict := make(map[string]dbus.Variant) + inVardict["session_handle_token"] = dbus.MakeVariant("t") + result, outVardict, err := getResponse(bus, remoteDesktop, + "org.freedesktop.portal.RemoteDesktop.CreateSession", 0, inVardict) if err != nil { - return nil, &UnsupportedPlatformError{ - fmt.Errorf("calling 'CreateSession' failed: %w", err), - } + return nil, UnsupportedPlatformError{err} } - sessionHandleString, ok := createSessionResults["session_handle"].Value().(string) + if result != 0 { + return nil, UnsupportedPlatformError{errors.New(fmt.Sprintf( + "Calling 'CreateSession' failed (%v)", result))} + } + sessionHandleV, ok := outVardict["session_handle"] if !ok { - return nil, &UnsupportedPlatformError{ - errors.New("unexpected 'session_handle' type in 'CreateSession' return value"), - } + return nil, UnsupportedPlatformError{errors.New( + "'session_handle' missing from 'CreateSession' return value")} } - sessionHandle := dbus.ObjectPath(sessionHandleString) - selectDevicesOptions := map[string]dbus.Variant{ - "types": dbus.MakeVariant(deviceKeyboard | devicePointer), + sessionHandleS, ok := sessionHandleV.Value().(string) + if !ok { + return nil, UnsupportedPlatformError{errors.New( + "unexpected 'session_handle' type in 'CreateSession' return value")} } - 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, - )) + sessionHandle := dbus.ObjectPath(sessionHandleS) + inVardict = make(map[string]dbus.Variant) + inVardict["type"] = dbus.MakeVariant(deviceKeyboard | devicePointer) + result, outVardict, err = getResponse(bus, remoteDesktop, + "org.freedesktop.portal.RemoteDesktop.SelectDevices", 0, sessionHandle, inVardict) if err != nil { - return nil, &UnsupportedPlatformError{ - fmt.Errorf("calling 'SelectDevices' failed: %w", err), - } + return nil, UnsupportedPlatformError{err} } - startResponseStatus, startResults, err := getResponse(bus, portalDesktop, - "org.freedesktop.portal.RemoteDesktop.Start", 0, - sessionHandle, "", map[string]dbus.Variant{}, - ) + if result != 0 { + return nil, UnsupportedPlatformError{errors.New(fmt.Sprintf( + "Calling 'SelectDevices' failed (%v)", result))} + } + inVardict = make(map[string]dbus.Variant) + result, outVardict, err = getResponse(bus, remoteDesktop, + "org.freedesktop.portal.RemoteDesktop.Start", 0, sessionHandle, "", inVardict) if err != nil { - return nil, &UnsupportedPlatformError{ - fmt.Errorf("calling 'Start' failed: %w", err), - } + return nil, UnsupportedPlatformError{err} } - if startResponseStatus != 0 { + if result != 0 { return nil, errors.New("keyboard or pointer access denied") } - if restoreToken, _ := startResults["restore_token"].Value().(string); restoreTokenStore != nil { - if err := restoreTokenStore.Store([]byte(restoreToken)); err != nil { - log.Printf("Failed to store restore token: %v", err) - } - } - devices, ok := startResults["devices"].Value().(uint32) + devicesV, ok := outVardict["devices"] if !ok { - return nil, &UnsupportedPlatformError{ - errors.New("unexpected 'devices' type in 'Start' return value"), - } + return nil, UnsupportedPlatformError{errors.New( + "'devices' missing from 'Start' return value")} + } + devices, ok := devicesV.Value().(uint32) + if !ok { + return nil, UnsupportedPlatformError{errors.New( + "unexpected 'devices' type in 'Start' return value")} } if devices&deviceKeyboard == 0 || devices&devicePointer == 0 { return nil, errors.New("keyboard or pointer access denied") } cleanupBus = false - return &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) + return &portalController{bus: bus, remoteDesktop: remoteDesktop, + sessionHandle: sessionHandle}, nil } func getResponse(bus *dbus.Conn, object dbus.BusObject, method string, - flags dbus.Flags, args ...interface{}, -) (uint32, map[string]dbus.Variant, error) { + flags dbus.Flags, args ...interface{}) (uint32, map[string]dbus.Variant, error) { ch := make(chan *dbus.Signal, 512) bus.Signal(ch) defer bus.RemoveSignal(ch) @@ -296,39 +161,47 @@ 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, fmt.Errorf("unexpected 'Response' return length (%d)", len(s.Body)) + return 0, nil, errors.New("unexpected 'Response' return length") } - responseStatus, ok := s.Body[0].(uint32) + result, ok := s.Body[0].(uint32) if !ok { return 0, nil, errors.New("unexpected 'Response' return type") } - results, ok := s.Body[1].(map[string]dbus.Variant) + outVardict, ok := s.Body[1].(map[string]dbus.Variant) if !ok { return 0, nil, errors.New("unexpected 'Response' return type") } - return responseStatus, results, nil + return result, outVardict, nil } } } -func checkResponse(responseStatus uint32, results map[string]dbus.Variant, err error) (map[string]dbus.Variant, error) { - if err == nil && responseStatus != 0 { - err = fmt.Errorf("unexpected 'Response' status (%d)", responseStatus) - } - return results, err -} - func (p *portalController) Close() error { - return p.bus.Close() + p.lock.Lock() + defer p.lock.Unlock() + if p.bus == nil { + return errors.New("dbus connection closed") + } + if err := p.bus.Close(); err != nil { + return err + } + p.bus = nil + p.remoteDesktop = nil + return nil } func (p *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.portalDesktop.Call( - "org.freedesktop.portal.RemoteDesktop.NotifyKeyboardKeysym", 0, - p.sessionHandle, map[string]dbus.Variant{}, keysym, state, - ).Store(); err != nil { + if err := p.remoteDesktop.Call( + "org.freedesktop.portal.RemoteDesktop.NotifyKeyboardKeysym", + 0, p.sessionHandle, inVardict, keysym, state).Store(); err != nil { return err } } @@ -358,42 +231,57 @@ 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 - switch button { - case PointerButtonLeft: + if button == PointerButtonLeft { btn = btnLeft - case PointerButtonMiddle: + } else if button == PointerButtonMiddle { btn = btnMiddle - case PointerButtonRight: + } else if button == PointerButtonRight { btn = btnRight - default: - return fmt.Errorf("unsupported pointer button: %#v", button) + } else { + return errors.New("unsupported pointer button") } state := btnReleased if press { state = btnPressed } - if err := p.portalDesktop.Call("org.freedesktop.portal.RemoteDesktop.NotifyPointerButton", 0, - p.sessionHandle, map[string]dbus.Variant{}, btn, state, - ).Store(); err != nil { + inVardict := make(map[string]dbus.Variant) + if err := p.remoteDesktop.Call("org.freedesktop.portal.RemoteDesktop.NotifyPointerButton", + 0, p.sessionHandle, inVardict, btn, state).Store(); err != nil { return err } return nil } func (p *portalController) PointerMove(deltaX, deltaY int) error { - if err := p.portalDesktop.Call("org.freedesktop.portal.RemoteDesktop.NotifyPointerMotion", 0, - p.sessionHandle, map[string]dbus.Variant{}, float64(deltaX), float64(deltaY), - ).Store(); err != nil { + p.lock.RLock() + defer p.lock.RUnlock() + if p.bus == nil { + return errors.New("dbus connection closed") + } + inVardict := make(map[string]dbus.Variant) + if err := p.remoteDesktop.Call("org.freedesktop.portal.RemoteDesktop.NotifyPointerMotion", + 0, p.sessionHandle, inVardict, float64(deltaX), float64(deltaY)).Store(); err != nil { return err } return nil } func (p *portalController) PointerScroll(deltaHorizontal, deltaVertical int, finish bool) error { - if err := p.portalDesktop.Call("org.freedesktop.portal.RemoteDesktop.NotifyPointerAxis", 0, - p.sessionHandle, map[string]dbus.Variant{"finish": dbus.MakeVariant(finish)}, float64(deltaHorizontal), float64(deltaVertical), - ).Store(); err != nil { + p.lock.RLock() + defer p.lock.RUnlock() + if p.bus == nil { + return errors.New("dbus connection closed") + } + inVardict := make(map[string]dbus.Variant) + inVardict["finish"] = dbus.MakeVariant(finish) + if err := p.remoteDesktop.Call("org.freedesktop.portal.RemoteDesktop.NotifyPointerAxis", + 0, p.sessionHandle, inVardict, float64(deltaHorizontal), float64(deltaVertical)).Store(); err != nil { return err } return nil diff --git a/inputcontrol/controller_uinput.go b/inputcontrol/controller_uinput.go deleted file mode 100644 index 2dd9bab..0000000 --- a/inputcontrol/controller_uinput.go +++ /dev/null @@ -1,200 +0,0 @@ -//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 58b5e9a..eb844b2 100644 --- a/inputcontrol/controller_windows.go +++ b/inputcontrol/controller_windows.go @@ -22,7 +22,7 @@ package inputcontrol import ( - "fmt" + "errors" "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,8 +111,9 @@ func (p *windowsController) sendInput(inputs []keybdInput) error { if len(inputs) == 0 { return nil } - if sent, _, err := sendInputProc.Call(uintptr(len(inputs)), - uintptr(unsafe.Pointer(&inputs[0])), unsafe.Sizeof(inputs[0])); int(sent) != len(inputs) { + if r, _, err := sendInputProc.Call(uintptr(len(inputs)), + uintptr(unsafe.Pointer(&inputs[0])), + unsafe.Sizeof(inputs[0])); int(r) != len(inputs) { return err } return nil @@ -134,45 +135,44 @@ func (p *windowsController) KeyboardText(text string) error { func (p *windowsController) KeyboardKey(key Key) error { input := keybdInput{typ: inputKeyboard} - switch key { - case KeyBackSpace: + if key == KeyBackSpace { input.wVk = vkBack - case KeyReturn: + } else if key == KeyReturn { input.wVk = vkReturn - case KeyEnd: + } else if key == KeyEnd { input.wVk = vkEnd - case KeyHome: + } else if key == KeyHome { input.wVk = vkHome - case KeyLeft: + } else if key == KeyLeft { input.wVk = vkLeft - case KeyUp: + } else if key == KeyUp { input.wVk = vkUp - case KeyRight: + } else if key == KeyRight { input.wVk = vkRight - case KeyDown: + } else if key == KeyDown { input.wVk = vkDown - case KeyDelete: + } else if key == KeyDelete { input.wVk = vkDelete - case KeySuper: + } else if key == KeySuper { input.wVk = vkLwin - case KeyBrowserBack: + } else if key == KeyBrowserBack { input.wVk = vkBrowserBack - case KeyBrowserForward: + } else if key == KeyBrowserForward { input.wVk = vkBrowserForward - case KeyVolumeMute: + } else if key == KeyVolumeMute { input.wVk = vkVolumeMute - case KeyVolumeDown: + } else if key == KeyVolumeDown { input.wVk = vkVolumeDown - case KeyVolumeUp: + } else if key == KeyVolumeUp { input.wVk = vkVolumeUp - case KeyMediaNextTrack: + } else if key == KeyMediaNextTrack { input.wVk = vkMediaNextTrack - case KeyMediaPrevTrack: + } else if key == KeyMediaPrevTrack { input.wVk = vkMediaPrevTrack - case KeyMediaPlayPause: + } else if key == KeyMediaPlayPause { input.wVk = vkMediaPlayPause - default: - return fmt.Errorf("key not mapped to virtual-key code: %#v", key) + } else { + return errors.New("key not mapped to virtual-key code") } 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 fmt.Errorf("unsupported pointer button: %#v", button) + return errors.New("unsupported pointer button") } - if sent, _, err := sendInputProc.Call(1, uintptr(unsafe.Pointer(&input)), - unsafe.Sizeof(input)); int(sent) != 1 { + if r, _, err := sendInputProc.Call(1, uintptr(unsafe.Pointer(&input)), + unsafe.Sizeof(input)); int(r) != 1 { return err } return nil @@ -210,8 +210,8 @@ func (p *windowsController) PointerMove(deltaX, deltaY int) error { dy: int32(deltaY), dwFlags: mouseeventfMove, } - if sent, _, err := sendInputProc.Call(1, uintptr(unsafe.Pointer(&input)), - unsafe.Sizeof(input)); int(sent) != 1 { + if r, _, err := sendInputProc.Call(1, uintptr(unsafe.Pointer(&input)), + unsafe.Sizeof(input)); int(r) != 1 { return err } return nil @@ -236,8 +236,9 @@ func (p *windowsController) PointerScroll(deltaHorizontal, deltaVertical int, fi if len(inputs) == 0 { return nil } - if sent, _, err := sendInputProc.Call(uintptr(len(inputs)), - uintptr(unsafe.Pointer(&inputs[0])), unsafe.Sizeof(inputs[0])); int(sent) != len(inputs) { + if r, _, err := sendInputProc.Call(uintptr(len(inputs)), + uintptr(unsafe.Pointer(&inputs[0])), + unsafe.Sizeof(inputs[0])); int(r) != len(inputs) { return err } return nil diff --git a/inputcontrol/controller_x11.go b/inputcontrol/controller_x11.go index f45b75b..3d7c9f7 100644 --- a/inputcontrol/controller_x11.go +++ b/inputcontrol/controller_x11.go @@ -21,42 +21,28 @@ package inputcontrol -// #cgo LDFLAGS: -lX11 -lXrandr -lXtst -// #include +// #cgo LDFLAGS: -lX11 -lXtst // #include // #include -// #include // #include // #include -// Window MacroDefaultRootWindow(Display *dpy) { -// return DefaultRootWindow(dpy); -// } import "C" - import ( "errors" "fmt" - "maps" - "slices" - "strings" + "os" "sync" "time" "unsafe" ) const ( - keyboardMappingDelay time.Duration = 200 * time.Millisecond + keyboardMappingDelay time.Duration = 500 * time.Millisecond scrollDiv int = 20 ) -var modifierIndexes = [...]int{ - C.ShiftMapIndex, - C.Mod1MapIndex, - C.Mod2MapIndex, - C.Mod3MapIndex, - C.Mod4MapIndex, - C.Mod5MapIndex, -} +var modifierIndices [6]uint = [...]uint{C.ShiftMapIndex, C.Mod1MapIndex, + C.Mod2MapIndex, C.Mod3MapIndex, C.Mod4MapIndex, C.Mod5MapIndex} type x11Controller struct { display *C.Display @@ -69,48 +55,17 @@ 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")} } - 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") + return &x11Controller{display: display}, nil } func (p *x11Controller) Close() error { @@ -124,68 +79,53 @@ func (p *x11Controller) Close() error { return nil } -func (p *x11Controller) findEmptyKeycodeLocked() (keycode C.KeyCode, keysymsPerKeycode int, err error) { +func (p *x11Controller) findEmptyKeycodeLocked() (C.KeyCode, C.int, error) { var minKeycodes, maxKeycodes C.int C.XDisplayKeycodes(p.display, &minKeycodes, &maxKeycodes) - var keysymsPerKeycodeC C.int - keysyms := C.XGetKeyboardMapping( - p.display, - C.KeyCode(minKeycodes), - maxKeycodes-minKeycodes+1, - &keysymsPerKeycodeC, - ) + var keysymsPerKeycode C.int + keysyms := C.XGetKeyboardMapping(p.display, C.KeyCode(minKeycodes), + maxKeycodes-minKeycodes+1, &keysymsPerKeycode) 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 := 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), - )) + 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))) if keysym != 0 { continue keycodes } } - return C.KeyCode(keycode), keysymsPerKeycode, nil + return keycode, keysymsPerKeycode, nil } return 0, 0, errors.New("no empty keycode found") } -func (p *x11Controller) changeKeyMappingLocked( - keycode C.KeyCode, keysymsPerKeycode int, keysym Keysym, -) { +func (p *x11Controller) changeKeyMappingLocked(keysymsPerKeycode C.int, + keycode C.KeyCode, keysym Keysym) { keycodeMapping := make([]C.KeySym, keysymsPerKeycode) for i := range keycodeMapping { keycodeMapping[i] = C.KeySym(keysym) } - C.XChangeKeyboardMapping( - p.display, - C.int(keycode), - C.int(keysymsPerKeycode), - (*C.KeySym)(unsafe.Pointer(&keycodeMapping[0])), - 1, - ) - C.XSync(p.display, C.False) + C.XChangeKeyboardMapping(p.display, C.int(keycode), keysymsPerKeycode, + (*C.KeySym)(unsafe.Pointer(&keycodeMapping[0])), 1) + C.XFlush(p.display) } 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 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), - )) + 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)))) 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 de183a1..5402120 100644 --- a/inputcontrol/keysyms.go +++ b/inputcontrol/keysyms.go @@ -23,7 +23,7 @@ package inputcontrol //go:generate go run keysyms.generator.go -import "fmt" +import "errors" 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, fmt.Errorf("rune not mappend to keysym and "+ - "out of range for direct unicode mapping: %q", runeValue) + return 0, errors.New("rune not mappend to keysym and " + + "out of range for direct unicode mapping") } keysym = Keysym(0x01000000 + runeValue) } @@ -66,44 +66,59 @@ func RuneToKeysym(runeValue rune) (Keysym, error) { } func KeyToKeysym(key Key) (Keysym, error) { - switch key { - case KeyBackSpace: + if key == KeyBackSpace { return xkBackSpace, nil - case KeyReturn: - return xkReturn, nil - case KeyDelete: - return xkDelete, nil - case KeyHome: - return xkHome, nil - case KeyLeft: - return xkLeft, nil - case KeyUp: - return xkUp, nil - case KeyRight: - return xkRight, nil - case KeyDown: - return xkDown, nil - case KeyEnd: - return xkEnd, nil - case KeySuper: - return xkSuperL, nil - case KeyVolumeMute: - return xf86xkAudioMute, nil - case KeyVolumeDown: - return xf86xkAudioLowerVolume, nil - case KeyVolumeUp: - return xf86xkAudioRaiseVolume, nil - case KeyMediaPlayPause: - return xf86xkAudioPlay, nil - case KeyMediaPrevTrack: - return xf86xkAudioPrev, nil - case KeyMediaNextTrack: - return xf86xkAudioNext, nil - case KeyBrowserBack: - return xf86xkBack, nil - case KeyBrowserForward: - return xf86xkForward, nil - default: - return 0, fmt.Errorf("key not mapped to keysym: %#v", key) } + if key == KeyReturn { + return xkReturn, nil + } + if key == KeyDelete { + return xkDelete, nil + } + if key == KeyHome { + return xkHome, nil + } + if key == KeyLeft { + return xkLeft, nil + } + if key == KeyUp { + return xkUp, nil + } + if key == KeyRight { + return xkRight, nil + } + if key == KeyDown { + return xkDown, nil + } + if key == KeyEnd { + return xkEnd, nil + } + if key == KeySuper { + return xkSuperL, nil + } + if key == KeyVolumeMute { + return xf86xkAudioMute, nil + } + if key == KeyVolumeDown { + return xf86xkAudioLowerVolume, nil + } + if key == KeyVolumeUp { + return xf86xkAudioRaiseVolume, nil + } + if key == KeyMediaPlayPause { + return xf86xkAudioPlay, nil + } + if key == KeyMediaPrevTrack { + return xf86xkAudioPrev, nil + } + if key == KeyMediaNextTrack { + return xf86xkAudioNext, nil + } + if key == KeyBrowserBack { + return xf86xkBack, nil + } + if key == KeyBrowserForward { + return xf86xkForward, nil + } + return 0, errors.New("key not mapped to keysym") } diff --git a/main.go b/main.go index f8db693..5a7e7a0 100644 --- a/main.go +++ b/main.go @@ -27,6 +27,9 @@ 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" @@ -36,10 +39,6 @@ import ( "strings" "time" "unicode/utf8" - - "github.com/unrud/remote-touchpad/inputcontrol" - "github.com/unrud/remote-touchpad/terminal" - "golang.org/x/net/websocket" ) const ( @@ -48,7 +47,7 @@ const ( authenticationRateBurst int = 10 challengeLength int = 8 defaultBind string = ":0" - version string = "1.5.4" + version string = "1.2.2" prettyAppName string = "Remote Touchpad" ) @@ -185,25 +184,21 @@ func main() { } var controller inputcontrol.Controller var controllerName string - var platformErrs []error + platformErrors := "" 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 { - 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) - } + log.Fatalf("%s controller: %v", controllerName, err) } } if controller == nil { - log.Fatal(fmt.Errorf("unsupported platform:\n%w", errors.Join(platformErrs...))) + log.Fatal("unsupported platform:\n" + platformErrors) } defer controller.Close() authenticationChallenges := make(chan challenge, authenticationRateBurst) @@ -246,7 +241,7 @@ func main() { return } if err := processCommand(controller, message); err != nil { - log.Print(fmt.Errorf("%s controller: %w", controllerName, err)) + log.Printf("%s controller: %v", controllerName, err) return } } diff --git a/remote-touchpad.service b/remote-touchpad.service deleted file mode 100644 index 76f5a74..0000000 --- a/remote-touchpad.service +++ /dev/null @@ -1,12 +0,0 @@ -[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 deleted file mode 100755 index 84fbe46..0000000 --- a/rts +++ /dev/null @@ -1,149 +0,0 @@ -#!/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 6d23326..f32c543 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -1,21 +1,17 @@ -# 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: core24 +base: core18 confinement: strict -platforms: - amd64: - arm64: - apps: remote-touchpad: command: bin/remote-touchpad @@ -24,13 +20,13 @@ apps: - desktop - x11 remote-touchpad-wait-on-error: - command: bin/remote-touchpad-wait-on-error + command: usr/bin/remote-touchpad-wait-on-error plugs: - network-bind - desktop - x11 common-id: com.github.unrud.RemoteTouchpad - desktop: usr/share/applications/com.github.unrud.RemoteTouchpad.desktop + desktop: share/applications/com.github.unrud.RemoteTouchpad.desktop parts: remote-touchpad: @@ -40,22 +36,18 @@ 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: | - 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 + 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 parse-info: [usr/share/metainfo/com.github.unrud.RemoteTouchpad.metainfo.xml] diff --git a/webdata.go b/webdata.go index 1b816b6..4d07131 100644 --- a/webdata.go +++ b/webdata.go @@ -1,52 +1,19 @@ -/* - * 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 deleted file mode 100644 index 8a26833..0000000 --- a/webdata/app/compat.mjs +++ /dev/null @@ -1,82 +0,0 @@ -/* - * 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 deleted file mode 100644 index a2984fb..0000000 --- a/webdata/app/inputcontroller.mjs +++ /dev/null @@ -1,121 +0,0 @@ -/* - * 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 deleted file mode 100644 index 84195d9..0000000 --- a/webdata/app/keyboard.mjs +++ /dev/null @@ -1,74 +0,0 @@ -/* - * 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 deleted file mode 100644 index 5477b89..0000000 --- a/webdata/app/main.mjs +++ /dev/null @@ -1,53 +0,0 @@ -/* - * 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 deleted file mode 100644 index 8158eb3..0000000 --- a/webdata/app/mouse.mjs +++ /dev/null @@ -1,74 +0,0 @@ -/* - * 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 deleted file mode 100644 index 3f7cd3f..0000000 --- a/webdata/app/sha256.mjs +++ /dev/null @@ -1,9 +0,0 @@ -/** - * 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 new file mode 100644 index 0000000..bf1d1ce --- /dev/null +++ b/webdata/fn.js @@ -0,0 +1,696 @@ +"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 52fb46f..ac1968d 100644 --- a/webdata/index.html +++ b/webdata/index.html @@ -1,8 +1,10 @@ - - + + + + Remote Touchpad @@ -11,90 +13,61 @@

    Connecting…

    - -
    + - -
    -

    Touchpad

    - - - + - -
    - + - -
    -
    -
    - - - +