diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..07c477a
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,11 @@
+# Binaries
+remote-touchpad
+
+# Systemd service state
+.remote-touchpad.service.state
+
+# Logs and run files
+.rts-log
+
+# Environment files
+.env
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6e06388..e59454c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,18 @@
# 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
diff --git a/README.md b/README.md
index fd05b7a..e039950 100644
--- a/README.md
+++ b/README.md
@@ -23,6 +23,35 @@ Supports Flatpak's RemoteDesktop portal (for Wayland), Windows and X11.
go install github.com/unrud/remote-touchpad@latest
```
+## Linux User Service Installation
+
+To install remote-touchpad as a user-specific systemd service (no root access required):
+
+1. Clone the repository and navigate to the project directory:
+
+ ```sh
+ git clone https://github.com/Unrud/remote-touchpad.git
+ cd remote-touchpad
+ ```
+
+2. Install the service using the provided wrapper script (no sudo required):
+
+ ```sh
+ ./rts install
+ ```
+
+3. Access the service:
+
+ * View service status: `./rts status`
+ * Get URL: `./rts url`
+ * View logs: `./rts log`
+ * Stop service: `./rts stop`
+ * Start service: `./rts start`
+ * Restart service: `./rts restart`
+ * Uninstall: `./rts uninstall`
+
+The service will run in the background and maintain a persistent connection with URL and QR code accessible via the `url` command.
+
## Screenshots

diff --git a/desktop/com.github.unrud.RemoteTouchpad.metainfo.xml b/desktop/com.github.unrud.RemoteTouchpad.metainfo.xml
index 9e2c36b..113ef4d 100644
--- a/desktop/com.github.unrud.RemoteTouchpad.metainfo.xml
+++ b/desktop/com.github.unrud.RemoteTouchpad.metainfo.xml
@@ -23,7 +23,22 @@
Unrud
-
+
+
+
+ - Improve X11 keyboard input
+ - Update dependencies
+
+
+
+
+
+
+
+
+
- Update dependencies
@@ -358,16 +373,16 @@
- https://raw.githubusercontent.com/Unrud/remote-touchpad/v1.5.1/screenshots/1.png
+ https://raw.githubusercontent.com/Unrud/remote-touchpad/v1.5.4/screenshots/1.png
- https://raw.githubusercontent.com/Unrud/remote-touchpad/v1.5.1/screenshots/2.png
+ https://raw.githubusercontent.com/Unrud/remote-touchpad/v1.5.4/screenshots/2.png
- https://raw.githubusercontent.com/Unrud/remote-touchpad/v1.5.1/screenshots/3.png
+ https://raw.githubusercontent.com/Unrud/remote-touchpad/v1.5.4/screenshots/3.png
- https://raw.githubusercontent.com/Unrud/remote-touchpad/v1.5.1/screenshots/4.png
+ https://raw.githubusercontent.com/Unrud/remote-touchpad/v1.5.4/screenshots/4.png
diff --git a/flatpak/com.github.unrud.RemoteTouchpad.yaml b/flatpak/com.github.unrud.RemoteTouchpad.yaml
index 54e8e97..239160e 100644
--- a/flatpak/com.github.unrud.RemoteTouchpad.yaml
+++ b/flatpak/com.github.unrud.RemoteTouchpad.yaml
@@ -17,8 +17,8 @@ modules:
path: ..
- type: git
url: https://github.com/godbus/dbus
- tag: v5.1.0
- commit: e523abc905595cf17fb0001a7d77eaaddfaa216d
+ tag: v5.2.2
+ commit: a8ac15ba63645f02ffd57f4b443203279ab40b30
dest: vendor/github.com/godbus/dbus/v5
- type: git
url: https://github.com/skip2/go-qrcode
@@ -26,14 +26,14 @@ modules:
dest: vendor/github.com/skip2/go-qrcode
- type: git
url: https://github.com/golang/net
- tag: v0.46.0
- commit: 2002a064a49010d8b881673c73f04e0b741e7c32
+ tag: v0.57.0
+ commit: b8f09f6f062ceb4531b7af4bd17a5c8fe9c4b2b5
dest: vendor/golang.org/x/net
- type: git
- url: https://github.com/golang/crypto
- tag: v0.43.0
- commit: 627cb894b6b2021e34c4ad4af4c0a963127491e4
- dest: vendor/golang.org/x/crypto
+ 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
diff --git a/flatpak/modules.txt b/flatpak/modules.txt
index 6662b65..6092904 100644
--- a/flatpak/modules.txt
+++ b/flatpak/modules.txt
@@ -1,17 +1,17 @@
# github.com/bendahl/uinput v1.7.0
## explicit; go 1.13
github.com/bendahl/uinput
-# github.com/godbus/dbus/v5 v5.1.0
-## explicit; go 1.12
+# 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/crypto v0.36.0
-## explicit; go 1.23.0
-golang.org/x/crypto/hkdf
-# golang.org/x/net v0.37.0
-## explicit; go 1.23.0
+# 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 75e1e34..5afd19d 100644
--- a/go.mod
+++ b/go.mod
@@ -1,13 +1,12 @@
module github.com/unrud/remote-touchpad
-go 1.24.0
-
-toolchain go1.24.8
+go 1.25.5
require (
github.com/bendahl/uinput v1.7.0
- github.com/godbus/dbus/v5 v5.1.0
+ github.com/godbus/dbus/v5 v5.2.2
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
- golang.org/x/crypto v0.43.0
- golang.org/x/net v0.46.0
+ golang.org/x/net v0.57.0
)
+
+require golang.org/x/sys v0.47.0 // indirect
diff --git a/go.sum b/go.sum
index 736cffa..783b4df 100644
--- a/go.sum
+++ b/go.sum
@@ -1,10 +1,10 @@
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.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
-github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
+github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
+github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
-golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
-golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
-golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
-golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
+golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
+golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
+golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
+golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
diff --git a/host.go b/host.go
index 54874e5..622b3bf 100644
--- a/host.go
+++ b/host.go
@@ -74,7 +74,8 @@ func findDefaultHost() string {
continue
}
for _, linkLocalPrefix := range []string{
- "169.254.", "fe8", "fe9", "fea", "feb"} {
+ "169.254.", "fe8", "fe9", "fea", "feb",
+ } {
if strings.HasPrefix(ip.String(), linkLocalPrefix) {
addIP(20, ip)
continue addrs
diff --git a/inputcontrol/controller.go b/inputcontrol/controller.go
index 06ed8e4..111e937 100644
--- a/inputcontrol/controller.go
+++ b/inputcontrol/controller.go
@@ -21,8 +21,10 @@ package inputcontrol
import "sort"
-type PointerButton int
-type Key int
+type (
+ PointerButton int
+ Key int
+)
const (
PointerButtonLeft PointerButton = iota
diff --git a/inputcontrol/controller_portal.go b/inputcontrol/controller_portal.go
index 7ee2f81..99ecf8b 100644
--- a/inputcontrol/controller_portal.go
+++ b/inputcontrol/controller_portal.go
@@ -24,6 +24,7 @@ package inputcontrol
import (
"crypto/aes"
"crypto/cipher"
+ "crypto/hkdf"
"crypto/rand"
"crypto/sha256"
"errors"
@@ -35,7 +36,6 @@ import (
"slices"
"github.com/godbus/dbus/v5"
- "golang.org/x/crypto/hkdf"
)
const (
@@ -88,12 +88,14 @@ func InitPortalController() (Controller, error) {
"org.freedesktop.portal.RemoteDesktop.version")
if err != nil {
return nil, &UnsupportedPlatformError{
- fmt.Errorf("getting 'version' failed: %w", err)}
+ fmt.Errorf("getting 'version' failed: %w", err),
+ }
}
remoteDesktopVersion, ok := remoteDesktopVersionV.Value().(uint32)
if !ok {
return nil, &UnsupportedPlatformError{
- errors.New("unexpected 'version' type")}
+ errors.New("unexpected 'version' type"),
+ }
}
restoreTokenStore, err := func() (*secretStore, error) {
if remoteDesktopVersion < 2 {
@@ -103,7 +105,7 @@ func InitPortalController() (Controller, error) {
if err != nil {
return nil, err
}
- if err := os.MkdirAll(cacheDirectory, 0700); err != nil {
+ if err := os.MkdirAll(cacheDirectory, 0o700); err != nil {
return nil, err
}
secret, err := retrieveSecret(bus)
@@ -120,17 +122,20 @@ func InitPortalController() (Controller, error) {
"org.freedesktop.portal.RemoteDesktop.AvailableDeviceTypes")
if err != nil {
return nil, &UnsupportedPlatformError{
- fmt.Errorf("getting 'AvailableDeviceTypes' failed: %w", err)}
+ fmt.Errorf("getting 'AvailableDeviceTypes' failed: %w", err),
+ }
}
availableDeviceTypes, ok := availableDeviceTypesV.Value().(uint32)
if !ok {
return nil, &UnsupportedPlatformError{
- errors.New("unexpected 'AvailableDeviceTypes' return type")}
+ errors.New("unexpected 'AvailableDeviceTypes' return type"),
+ }
}
if availableDeviceTypes&deviceKeyboard == 0 ||
availableDeviceTypes&devicePointer == 0 {
return nil, &UnsupportedPlatformError{
- errors.New("keyboard or pointer source type not supported")}
+ errors.New("keyboard or pointer source type not supported"),
+ }
}
createSessionResults, err := checkResponse(getResponse(bus, portalDesktop,
"org.freedesktop.portal.RemoteDesktop.CreateSession", 0,
@@ -138,12 +143,14 @@ func InitPortalController() (Controller, error) {
))
if err != nil {
return nil, &UnsupportedPlatformError{
- fmt.Errorf("calling 'CreateSession' failed: %w", err)}
+ fmt.Errorf("calling 'CreateSession' failed: %w", err),
+ }
}
sessionHandleString, ok := createSessionResults["session_handle"].Value().(string)
if !ok {
return nil, &UnsupportedPlatformError{
- errors.New("unexpected 'session_handle' type in 'CreateSession' return value")}
+ errors.New("unexpected 'session_handle' type in 'CreateSession' return value"),
+ }
}
sessionHandle := dbus.ObjectPath(sessionHandleString)
selectDevicesOptions := map[string]dbus.Variant{
@@ -165,7 +172,8 @@ func InitPortalController() (Controller, error) {
))
if err != nil {
return nil, &UnsupportedPlatformError{
- fmt.Errorf("calling 'SelectDevices' failed: %w", err)}
+ fmt.Errorf("calling 'SelectDevices' failed: %w", err),
+ }
}
startResponseStatus, startResults, err := getResponse(bus, portalDesktop,
"org.freedesktop.portal.RemoteDesktop.Start", 0,
@@ -173,7 +181,8 @@ func InitPortalController() (Controller, error) {
)
if err != nil {
return nil, &UnsupportedPlatformError{
- fmt.Errorf("calling 'Start' failed: %w", err)}
+ fmt.Errorf("calling 'Start' failed: %w", err),
+ }
}
if startResponseStatus != 0 {
return nil, errors.New("keyboard or pointer access denied")
@@ -186,14 +195,17 @@ func InitPortalController() (Controller, error) {
devices, ok := startResults["devices"].Value().(uint32)
if !ok {
return nil, &UnsupportedPlatformError{
- errors.New("unexpected 'devices' type in 'Start' return value")}
+ errors.New("unexpected 'devices' type in 'Start' return value"),
+ }
}
if devices&deviceKeyboard == 0 || devices&devicePointer == 0 {
return nil, errors.New("keyboard or pointer access denied")
}
cleanupBus = false
- return &portalController{bus: bus, portalDesktop: portalDesktop,
- sessionHandle: sessionHandle}, nil
+ return &portalController{
+ bus: bus, portalDesktop: portalDesktop,
+ sessionHandle: sessionHandle,
+ }, nil
}
func retrieveSecret(bus *dbus.Conn) ([]byte, error) {
@@ -230,9 +242,8 @@ type secretStore struct {
}
func newSecretStore(key []byte, filename string) (*secretStore, error) {
- hkdf := hkdf.New(sha256.New, key, nil, nil)
- derivedKey := make([]byte, 32)
- if _, err := io.ReadFull(hkdf, derivedKey); err != nil {
+ derivedKey, err := hkdf.Key(sha256.New, key, nil, "", 32)
+ if err != nil {
return nil, err
}
block, err := aes.NewCipher(derivedKey)
@@ -268,11 +279,12 @@ func (s *secretStore) Store(data []byte) error {
return err
}
ciphertext := s.aesgcm.Seal(nil, nonce, data, nil)
- return os.WriteFile(s.filename, slices.Concat(nonce, ciphertext), 0600)
+ return os.WriteFile(s.filename, slices.Concat(nonce, ciphertext), 0o600)
}
func getResponse(bus *dbus.Conn, object dbus.BusObject, method string,
- flags dbus.Flags, args ...interface{}) (uint32, map[string]dbus.Variant, error) {
+ flags dbus.Flags, args ...interface{},
+) (uint32, map[string]dbus.Variant, error) {
ch := make(chan *dbus.Signal, 512)
bus.Signal(ch)
defer bus.RemoveSignal(ch)
diff --git a/inputcontrol/controller_uinput.go b/inputcontrol/controller_uinput.go
index df51e65..2dd9bab 100644
--- a/inputcontrol/controller_uinput.go
+++ b/inputcontrol/controller_uinput.go
@@ -25,10 +25,11 @@ package inputcontrol
import (
"errors"
"fmt"
- "github.com/bendahl/uinput"
"log"
"os"
"time"
+
+ "github.com/bendahl/uinput"
)
const shiftKeysDelay time.Duration = 50 * time.Millisecond
diff --git a/inputcontrol/controller_x11.go b/inputcontrol/controller_x11.go
index bb37d37..f45b75b 100644
--- a/inputcontrol/controller_x11.go
+++ b/inputcontrol/controller_x11.go
@@ -32,9 +32,12 @@ package inputcontrol
// return DefaultRootWindow(dpy);
// }
import "C"
+
import (
"errors"
"fmt"
+ "maps"
+ "slices"
"strings"
"sync"
"time"
@@ -42,12 +45,18 @@ import (
)
const (
- keyboardMappingDelay time.Duration = 500 * time.Millisecond
+ keyboardMappingDelay time.Duration = 200 * time.Millisecond
scrollDiv int = 20
)
-var modifierIndices [6]uint = [...]uint{C.ShiftMapIndex, C.Mod1MapIndex,
- C.Mod2MapIndex, C.Mod3MapIndex, C.Mod4MapIndex, C.Mod5MapIndex}
+var modifierIndexes = [...]int{
+ C.ShiftMapIndex,
+ C.Mod1MapIndex,
+ C.Mod2MapIndex,
+ C.Mod3MapIndex,
+ C.Mod4MapIndex,
+ C.Mod5MapIndex,
+}
type x11Controller struct {
display *C.Display
@@ -63,13 +72,15 @@ 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
}
@@ -113,53 +124,68 @@ func (p *x11Controller) Close() error {
return nil
}
-func (p *x11Controller) findEmptyKeycodeLocked() (C.KeyCode, C.int, error) {
+func (p *x11Controller) findEmptyKeycodeLocked() (keycode C.KeyCode, keysymsPerKeycode int, err error) {
var minKeycodes, maxKeycodes C.int
C.XDisplayKeycodes(p.display, &minKeycodes, &maxKeycodes)
- var keysymsPerKeycode C.int
- keysyms := C.XGetKeyboardMapping(p.display, C.KeyCode(minKeycodes),
- maxKeycodes-minKeycodes+1, &keysymsPerKeycode)
+ var keysymsPerKeycodeC C.int
+ keysyms := C.XGetKeyboardMapping(
+ p.display,
+ C.KeyCode(minKeycodes),
+ maxKeycodes-minKeycodes+1,
+ &keysymsPerKeycodeC,
+ )
if keysyms == nil {
return 0, 0, errors.New("failed to get keyboard mapping")
}
defer C.XFree(unsafe.Pointer(keysyms))
+ keysymsPerKeycode = int(keysymsPerKeycodeC)
keycodes:
- for keycode := C.KeyCode(minKeycodes); keycode <= C.KeyCode(maxKeycodes); keycode++ {
- for i := 0; i < int(keysymsPerKeycode); i++ {
- keysymsIndex := int(keycode-
- C.KeyCode(minKeycodes))*int(keysymsPerKeycode) + i
- keysym := *(*C.KeySym)(unsafe.Pointer(uintptr(unsafe.Pointer(keysyms)) +
- uintptr(keysymsIndex)*unsafe.Sizeof(*keysyms)))
+ for keycode := int(minKeycodes); keycode <= int(maxKeycodes); keycode++ {
+ for i := range keysymsPerKeycode {
+ keysymsIndex := (keycode-int(minKeycodes))*keysymsPerKeycode + i
+ keysym := *(*C.KeySym)(unsafe.Pointer(
+ uintptr(unsafe.Pointer(keysyms)) +
+ uintptr(keysymsIndex)*unsafe.Sizeof(*keysyms),
+ ))
if keysym != 0 {
continue keycodes
}
}
- return keycode, keysymsPerKeycode, nil
+ return C.KeyCode(keycode), keysymsPerKeycode, nil
}
return 0, 0, errors.New("no empty keycode found")
}
-func (p *x11Controller) changeKeyMappingLocked(keysymsPerKeycode C.int,
- keycode C.KeyCode, keysym Keysym) {
+func (p *x11Controller) changeKeyMappingLocked(
+ keycode C.KeyCode, keysymsPerKeycode int, keysym Keysym,
+) {
keycodeMapping := make([]C.KeySym, keysymsPerKeycode)
for i := range keycodeMapping {
keycodeMapping[i] = C.KeySym(keysym)
}
- C.XChangeKeyboardMapping(p.display, C.int(keycode), keysymsPerKeycode,
- (*C.KeySym)(unsafe.Pointer(&keycodeMapping[0])), 1)
- C.XFlush(p.display)
+ C.XChangeKeyboardMapping(
+ p.display,
+ C.int(keycode),
+ C.int(keysymsPerKeycode),
+ (*C.KeySym)(unsafe.Pointer(&keycodeMapping[0])),
+ 1,
+ )
+ C.XSync(p.display, C.False)
}
func (p *x11Controller) getModKeycodesLocked() map[uint]C.KeyCode {
modKeymap := C.XGetModifierMapping(p.display)
defer C.XFreeModifiermap(modKeymap)
modKeycodes := make(map[uint]C.KeyCode)
- for _, modIndex := range modifierIndices {
- for i := 0; i < int(modKeymap.max_keypermod); i++ {
- keycode := *(*C.KeyCode)(unsafe.Pointer(uintptr(unsafe.Pointer(modKeymap.modifiermap)) +
- uintptr(uint(modIndex)*uint(modKeymap.max_keypermod)+uint(i))))
+ for _, modIndex := range modifierIndexes {
+ for i := range int(modKeymap.max_keypermod) {
+ keycodeIndex := modIndex*int(modKeymap.max_keypermod) + i
+ keycode := *(*C.KeyCode)(unsafe.Pointer(
+ uintptr(unsafe.Pointer(modKeymap.modifiermap)) +
+ uintptr(keycodeIndex)*unsafe.Sizeof(*modKeymap.modifiermap),
+ ))
if keycode != 0 {
- modKeycodes[1< /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 677f302..6d23326 100644
--- a/snap/snapcraft.yaml
+++ b/snap/snapcraft.yaml
@@ -1,17 +1,21 @@
-# Build by running "snapcraft".
+# Build by running `snapcraft`.
# WARNING:
# Snapcraft uses caching for already build steps but it's buggy and can cause strange problems.
-# Clean the cache by running "snapcraft clean".
+# Clean the cache by running `snapcraft clean`.
name: remote-touchpad
license: GPL-3.0+
grade: stable
adopt-info: remote-touchpad
-base: core22
+base: core24
confinement: strict
+platforms:
+ amd64:
+ arm64:
+
apps:
remote-touchpad:
command: bin/remote-touchpad
@@ -48,10 +52,6 @@ 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