diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 9f3768a..c1ed46d 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -23,7 +23,7 @@ jobs: with: go-version: stable check-latest: true - - run: sudo apt install libxrandr-dev libxtst-dev libxt-dev + - run: sudo apt install libxrandr-dev libxtst-dev if: ${{ contains(matrix.tags, 'x11') }} - name: Go Format 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..bc0a1d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,27 +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 diff --git a/README.md b/README.md index e039950..2740e73 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 & uinput & X11: `go install -tags portal,uinput,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..4618317 100644 --- a/desktop/com.github.unrud.RemoteTouchpad.metainfo.xml +++ b/desktop/com.github.unrud.RemoteTouchpad.metainfo.xml @@ -23,43 +23,6 @@ Unrud - - -
    -
  • Improve X11 keyboard input
  • -
  • Update dependencies
  • -
-
-
- - -
    -
  • Update dependencies
  • -
-
-
- - -
    -
  • Update dependencies
  • -
-
-
- - -
    -
  • Add restore token support for the remote desktop portal
  • -
  • Update dependencies
  • -
-
-
- - -
    -
  • Update dependencies
  • -
-
-
    @@ -373,16 +336,16 @@ - https://raw.githubusercontent.com/Unrud/remote-touchpad/v1.5.4/screenshots/1.png + https://raw.githubusercontent.com/Unrud/remote-touchpad/v1.4.7/screenshots/1.png - https://raw.githubusercontent.com/Unrud/remote-touchpad/v1.5.4/screenshots/2.png + https://raw.githubusercontent.com/Unrud/remote-touchpad/v1.4.7/screenshots/2.png - https://raw.githubusercontent.com/Unrud/remote-touchpad/v1.5.4/screenshots/3.png + https://raw.githubusercontent.com/Unrud/remote-touchpad/v1.4.7/screenshots/3.png - https://raw.githubusercontent.com/Unrud/remote-touchpad/v1.5.4/screenshots/4.png + https://raw.githubusercontent.com/Unrud/remote-touchpad/v1.4.7/screenshots/4.png diff --git a/flatpak/com.github.unrud.RemoteTouchpad.yaml b/flatpak/com.github.unrud.RemoteTouchpad.yaml index 239160e..0413e96 100644 --- a/flatpak/com.github.unrud.RemoteTouchpad.yaml +++ b/flatpak/com.github.unrud.RemoteTouchpad.yaml @@ -1,6 +1,6 @@ app-id: com.github.unrud.RemoteTouchpad runtime: org.freedesktop.Platform -runtime-version: '25.08' +runtime-version: '23.08' sdk: org.freedesktop.Sdk command: remote-touchpad sdk-extensions: @@ -8,17 +8,19 @@ sdk-extensions: finish-args: - --socket=fallback-x11 - --socket=wayland - - --share=ipc # Required by Flathub linter - --share=network modules: - name: remote-touchpad sources: - type: git path: .. + - type: shell + commands: + - . /usr/lib/sdk/golang/enable.sh; go mod vendor -e - type: git url: https://github.com/godbus/dbus - tag: v5.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,9 @@ modules: dest: vendor/github.com/skip2/go-qrcode - type: git url: https://github.com/golang/net - tag: v0.57.0 - commit: b8f09f6f062ceb4531b7af4bd17a5c8fe9c4b2b5 + tag: v0.28.0 + commit: 4542a42604cd159f1adb93c58368079ae37b3bf6 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..50d9d86 100644 --- a/go.mod +++ b/go.mod @@ -1,12 +1,10 @@ module github.com/unrud/remote-touchpad -go 1.25.5 +go 1.20 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.28.0 ) - -require golang.org/x/sys v0.47.0 // indirect diff --git a/go.sum b/go.sum index 783b4df..8a1a094 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,8 @@ 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.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= 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..06ed8e4 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 diff --git a/inputcontrol/controller_portal.go b/inputcontrol/controller_portal.go index 99ecf8b..7d19efc 100644 --- a/inputcontrol/controller_portal.go +++ b/inputcontrol/controller_portal.go @@ -22,19 +22,8 @@ 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" ) @@ -45,8 +34,6 @@ const ( btnReleased uint32 = 0 btnPressed uint32 = 1 - untilRevoked uint32 = 2 - // linux/input-event-codes.h btnLeft int32 = 0x110 btnRight int32 = 0x111 @@ -55,7 +42,7 @@ const ( type portalController struct { bus *dbus.Conn - portalDesktop dbus.BusObject + remoteDesktop dbus.BusObject sessionHandle dbus.ObjectPath } @@ -82,209 +69,85 @@ func InitPortalController() (Controller, error) { if err != nil { 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"), - } + errors.New("unexpected 'AvailableDeviceTypes' return type")} } if availableDeviceTypes&deviceKeyboard == 0 || availableDeviceTypes&devicePointer == 0 { return nil, &UnsupportedPlatformError{ - errors.New("keyboard or pointer source type not supported"), - } + errors.New("keyboard or pointer source type not supported")} } - 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{ + fmt.Errorf("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"), - } + errors.New("'session_handle' missing from 'CreateSession' return value")} } - sessionHandle := dbus.ObjectPath(sessionHandleString) - selectDevicesOptions := map[string]dbus.Variant{ - "types": dbus.MakeVariant(deviceKeyboard | devicePointer), - } - if restoreTokenStore != nil { - if restoreToken, err := restoreTokenStore.Load(); err != nil { - if !errors.Is(err, os.ErrNotExist) { - log.Printf("Failed to load restore token: %v", err) - } - } else if len(restoreToken) > 0 { - selectDevicesOptions["restore_token"] = dbus.MakeVariant(string(restoreToken)) - } - selectDevicesOptions["persist_mode"] = dbus.MakeVariant(untilRevoked) - } - _, err = checkResponse(getResponse(bus, portalDesktop, - "org.freedesktop.portal.RemoteDesktop.SelectDevices", 0, - sessionHandle, selectDevicesOptions, - )) - if err != nil { + sessionHandleS, ok := sessionHandleV.Value().(string) + if !ok { return nil, &UnsupportedPlatformError{ - fmt.Errorf("calling 'SelectDevices' failed: %w", err), - } + errors.New("unexpected 'session_handle' type in 'CreateSession' return value")} } - startResponseStatus, startResults, err := getResponse(bus, portalDesktop, - "org.freedesktop.portal.RemoteDesktop.Start", 0, - sessionHandle, "", map[string]dbus.Variant{}, - ) + sessionHandle := dbus.ObjectPath(sessionHandleS) + inVardict = make(map[string]dbus.Variant) + inVardict["types"] = 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 'Start' failed: %w", err), - } + return nil, &UnsupportedPlatformError{err} } - if startResponseStatus != 0 { + if result != 0 { + return nil, &UnsupportedPlatformError{ + fmt.Errorf("Calling 'SelectDevices' failed (%v)", result)} + } + inVardict = make(map[string]dbus.Variant) + result, outVardict, err = getResponse(bus, remoteDesktop, + "org.freedesktop.portal.RemoteDesktop.Start", 0, sessionHandle, "", inVardict) + if err != nil { + return nil, &UnsupportedPlatformError{err} + } + if result != 0 { return nil, errors.New("keyboard or pointer access denied") } - 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"), - } + 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 +159,32 @@ 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() } func (p *portalController) keyboardKeys(keys []Keysym) error { + inVardict := make(map[string]dbus.Variant) for _, keysym := range keys { for _, state := range [...]uint32{btnPressed, btnReleased} { - if err := p.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 } } @@ -373,27 +229,28 @@ func (p *portalController) PointerButton(button PointerButton, press bool) error 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 { + 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 { + 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 index 2dd9bab..df51e65 100644 --- a/inputcontrol/controller_uinput.go +++ b/inputcontrol/controller_uinput.go @@ -25,11 +25,10 @@ package inputcontrol import ( "errors" "fmt" + "github.com/bendahl/uinput" "log" "os" "time" - - "github.com/bendahl/uinput" ) const shiftKeysDelay time.Duration = 50 * time.Millisecond diff --git a/inputcontrol/controller_x11.go b/inputcontrol/controller_x11.go index f45b75b..bb37d37 100644 --- a/inputcontrol/controller_x11.go +++ b/inputcontrol/controller_x11.go @@ -32,12 +32,9 @@ package inputcontrol // return DefaultRootWindow(dpy); // } import "C" - import ( "errors" "fmt" - "maps" - "slices" "strings" "sync" "time" @@ -45,18 +42,12 @@ import ( ) 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 @@ -72,15 +63,13 @@ func InitX11Controller() (Controller, error) { display := C.XOpenDisplay(nil) if display == nil { return nil, &UnsupportedPlatformError{ - errors.New("failed to connect to X server"), - } + errors.New("failed to connect to X server")} } p := &x11Controller{display: display} if p.xIsXwayland() { p.Close() return nil, &UnsupportedPlatformError{ - errors.New("X server is Xwayland"), - } + errors.New("X server is Xwayland")} } return p, nil } @@ -124,68 +113,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< /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..677f302 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: core22 confinement: strict -platforms: - amd64: - arm64: - apps: remote-touchpad: command: bin/remote-touchpad @@ -52,6 +48,10 @@ parts: - libxt6 - libxrandr2 - libxtst6 + override-pull: | + craftctl default + # WORKAROUND: Point icon directly to SVG otherwise snapcraft can't find it + sed -e 's|Icon=com.github.unrud.RemoteTouchpad|Icon=/usr/share/icons/hicolor/scalable/apps/com.github.unrud.RemoteTouchpad.svg|' -i desktop/com.github.unrud.RemoteTouchpad.desktop override-build: | craftctl default install -Dm0755 -t "${CRAFT_PART_INSTALL}/bin" desktop/remote-touchpad-wait-on-error