Compare commits
39 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2ac00c3b48 | |||
| ed7e8bae54 | |||
|
|
a77f3aa8ec | ||
|
|
9764d1975d | ||
|
|
5e68074781 | ||
|
|
f80b1d6ef7 | ||
|
|
346d9cf375 | ||
|
|
6ef2be8776 | ||
|
|
85b6d6ea9d | ||
|
|
1aa8f92cc8 | ||
|
|
996edb0501 | ||
|
|
a30bed4624 | ||
|
|
a2dfa7ac47 | ||
|
|
eebde58826 | ||
|
|
800171efdd | ||
|
|
2b3d5c5bf3 | ||
|
|
1add8e4cb8 | ||
|
|
06674b224f | ||
|
|
d48d9aed3d | ||
|
|
5771f44b0e | ||
|
|
16dc47b570 | ||
|
|
abd9fd4773 | ||
|
|
7c1652e9dd | ||
|
|
0fe41d0b15 | ||
|
|
21461d9a76 | ||
|
|
c6c54c77a7 | ||
|
|
7a806f7b00 | ||
|
|
4fb452c542 | ||
|
|
c71e5c4b8b | ||
|
|
30cfff86b0 | ||
|
|
288345b5d3 | ||
|
|
b4ecfad316 | ||
|
|
dd510bc4a0 | ||
|
|
4916fc59db | ||
|
|
064c3cd242 | ||
|
|
6996fbf958 | ||
|
|
016351e9d2 | ||
|
|
6325782058 | ||
|
|
0184dcb22f |
20 changed files with 734 additions and 176 deletions
6
.github/workflows/test.yaml
vendored
6
.github/workflows/test.yaml
vendored
|
|
@ -23,7 +23,7 @@ jobs:
|
||||||
with:
|
with:
|
||||||
go-version: stable
|
go-version: stable
|
||||||
check-latest: true
|
check-latest: true
|
||||||
- run: sudo apt install libxrandr-dev libxtst-dev
|
- run: sudo apt install libxrandr-dev libxtst-dev libxt-dev
|
||||||
if: ${{ contains(matrix.tags, 'x11') }}
|
if: ${{ contains(matrix.tags, 'x11') }}
|
||||||
|
|
||||||
- name: Go Format
|
- name: Go Format
|
||||||
|
|
@ -38,10 +38,6 @@ jobs:
|
||||||
- name: Go Build
|
- name: Go Build
|
||||||
run: go build ./...
|
run: go build ./...
|
||||||
|
|
||||||
- name: Go Test
|
|
||||||
run: go test ./...
|
|
||||||
if: ${{ matrix.os != 'linux' }}
|
|
||||||
|
|
||||||
- name: Go Test (with data race detection)
|
- name: Go Test (with data race detection)
|
||||||
run: go test -race ./...
|
run: go test -race ./...
|
||||||
if: ${{ matrix.os == 'linux' }}
|
if: ${{ matrix.os == 'linux' }}
|
||||||
|
|
|
||||||
11
.gitignore
vendored
Normal file
11
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
# Binaries
|
||||||
|
remote-touchpad
|
||||||
|
|
||||||
|
# Systemd service state
|
||||||
|
.remote-touchpad.service.state
|
||||||
|
|
||||||
|
# Logs and run files
|
||||||
|
.rts-log
|
||||||
|
|
||||||
|
# Environment files
|
||||||
|
.env
|
||||||
30
CHANGELOG.md
30
CHANGELOG.md
|
|
@ -1,5 +1,35 @@
|
||||||
# Changelog
|
# 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)
|
## 1.4.5 (2024-01-31)
|
||||||
|
|
||||||
* Keep text input open after sending
|
* Keep text input open after sending
|
||||||
|
|
|
||||||
41
README.md
41
README.md
|
|
@ -12,8 +12,45 @@ Supports Flatpak's RemoteDesktop portal (for Wayland), Windows and X11.
|
||||||
* [Snap](https://snapcraft.io/remote-touchpad)
|
* [Snap](https://snapcraft.io/remote-touchpad)
|
||||||
* [Windows](https://github.com/Unrud/remote-touchpad/releases/latest)
|
* [Windows](https://github.com/Unrud/remote-touchpad/releases/latest)
|
||||||
* Golang:
|
* Golang:
|
||||||
* Portal & uinput & X11: `go install -tags portal,uinput,x11 github.com/unrud/remote-touchpad@latest`
|
* Portal & uinput & X11:
|
||||||
* Windows: `go install github.com/unrud/remote-touchpad@latest`
|
|
||||||
|
```sh
|
||||||
|
go install -tags portal,uinput,x11 github.com/unrud/remote-touchpad@latest
|
||||||
|
```
|
||||||
|
* Windows:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go install github.com/unrud/remote-touchpad@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
## Linux User Service Installation
|
||||||
|
|
||||||
|
To install remote-touchpad as a user-specific systemd service (no root access required):
|
||||||
|
|
||||||
|
1. Clone the repository and navigate to the project directory:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git clone https://github.com/Unrud/remote-touchpad.git
|
||||||
|
cd remote-touchpad
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Install the service using the provided wrapper script (no sudo required):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./rts install
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Access the service:
|
||||||
|
|
||||||
|
* View service status: `./rts status`
|
||||||
|
* Get URL: `./rts url`
|
||||||
|
* View logs: `./rts log`
|
||||||
|
* Stop service: `./rts stop`
|
||||||
|
* Start service: `./rts start`
|
||||||
|
* Restart service: `./rts restart`
|
||||||
|
* Uninstall: `./rts uninstall`
|
||||||
|
|
||||||
|
The service will run in the background and maintain a persistent connection with URL and QR code accessible via the `url` command.
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,63 @@
|
||||||
<launchable type="desktop-id">com.github.unrud.RemoteTouchpad.desktop</launchable>
|
<launchable type="desktop-id">com.github.unrud.RemoteTouchpad.desktop</launchable>
|
||||||
<url type="bugtracker">https://github.com/Unrud/remote-touchpad/issues</url>
|
<url type="bugtracker">https://github.com/Unrud/remote-touchpad/issues</url>
|
||||||
<url type="homepage">https://github.com/Unrud/remote-touchpad</url>
|
<url type="homepage">https://github.com/Unrud/remote-touchpad</url>
|
||||||
<developer_name>Unrud</developer_name>
|
<url type="vcs-browser">https://github.com/Unrud/remote-touchpad</url>
|
||||||
|
<developer_name translate="no">Unrud</developer_name>
|
||||||
|
<developer id="io.github.unrud">
|
||||||
|
<name translate="no">Unrud</name>
|
||||||
|
</developer>
|
||||||
<releases>
|
<releases>
|
||||||
|
<release version="1.5.4" date="2026-08-02">
|
||||||
|
<description>
|
||||||
|
<ul>
|
||||||
|
<li>Improve X11 keyboard input</li>
|
||||||
|
<li>Update dependencies</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="1.5.3" date="2025-12-24">
|
||||||
|
<description>
|
||||||
|
<ul>
|
||||||
|
<li>Update dependencies</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="1.5.2" date="2025-10-12">
|
||||||
|
<description>
|
||||||
|
<ul>
|
||||||
|
<li>Update dependencies</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="1.5.0" date="2025-03-18">
|
||||||
|
<description>
|
||||||
|
<ul>
|
||||||
|
<li>Add restore token support for the remote desktop portal</li>
|
||||||
|
<li>Update dependencies</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="1.4.8" date="2024-09-01">
|
||||||
|
<description>
|
||||||
|
<ul>
|
||||||
|
<li>Update dependencies</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="1.4.7" date="2024-08-30">
|
||||||
|
<description>
|
||||||
|
<ul>
|
||||||
|
<li>Update dependencies</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="1.4.6" date="2024-02-06">
|
||||||
|
<description>
|
||||||
|
<ul>
|
||||||
|
<li>Prevent incorrect MIME types</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
<release version="1.4.5" date="2024-01-31">
|
<release version="1.4.5" date="2024-01-31">
|
||||||
<description>
|
<description>
|
||||||
<ul>
|
<ul>
|
||||||
|
|
@ -318,16 +373,16 @@
|
||||||
</releases>
|
</releases>
|
||||||
<screenshots>
|
<screenshots>
|
||||||
<screenshot type="default">
|
<screenshot type="default">
|
||||||
<image>https://raw.githubusercontent.com/Unrud/remote-touchpad/v1.4.5/screenshots/1.png</image>
|
<image>https://raw.githubusercontent.com/Unrud/remote-touchpad/v1.5.4/screenshots/1.png</image>
|
||||||
</screenshot>
|
</screenshot>
|
||||||
<screenshot>
|
<screenshot>
|
||||||
<image>https://raw.githubusercontent.com/Unrud/remote-touchpad/v1.4.5/screenshots/2.png</image>
|
<image>https://raw.githubusercontent.com/Unrud/remote-touchpad/v1.5.4/screenshots/2.png</image>
|
||||||
</screenshot>
|
</screenshot>
|
||||||
<screenshot>
|
<screenshot>
|
||||||
<image>https://raw.githubusercontent.com/Unrud/remote-touchpad/v1.4.5/screenshots/3.png</image>
|
<image>https://raw.githubusercontent.com/Unrud/remote-touchpad/v1.5.4/screenshots/3.png</image>
|
||||||
</screenshot>
|
</screenshot>
|
||||||
<screenshot>
|
<screenshot>
|
||||||
<image>https://raw.githubusercontent.com/Unrud/remote-touchpad/v1.4.5/screenshots/4.png</image>
|
<image>https://raw.githubusercontent.com/Unrud/remote-touchpad/v1.5.4/screenshots/4.png</image>
|
||||||
</screenshot>
|
</screenshot>
|
||||||
</screenshots>
|
</screenshots>
|
||||||
<content_rating type="oars-1.1"/>
|
<content_rating type="oars-1.1"/>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
app-id: com.github.unrud.RemoteTouchpad
|
app-id: com.github.unrud.RemoteTouchpad
|
||||||
runtime: org.freedesktop.Platform
|
runtime: org.freedesktop.Platform
|
||||||
runtime-version: '23.08'
|
runtime-version: '25.08'
|
||||||
sdk: org.freedesktop.Sdk
|
sdk: org.freedesktop.Sdk
|
||||||
command: remote-touchpad
|
command: remote-touchpad
|
||||||
sdk-extensions:
|
sdk-extensions:
|
||||||
|
|
@ -8,19 +8,17 @@ sdk-extensions:
|
||||||
finish-args:
|
finish-args:
|
||||||
- --socket=fallback-x11
|
- --socket=fallback-x11
|
||||||
- --socket=wayland
|
- --socket=wayland
|
||||||
|
- --share=ipc # Required by Flathub linter
|
||||||
- --share=network
|
- --share=network
|
||||||
modules:
|
modules:
|
||||||
- name: remote-touchpad
|
- name: remote-touchpad
|
||||||
sources:
|
sources:
|
||||||
- type: git
|
- type: git
|
||||||
path: ..
|
path: ..
|
||||||
- type: shell
|
|
||||||
commands:
|
|
||||||
- . /usr/lib/sdk/golang/enable.sh; go mod vendor -e
|
|
||||||
- type: git
|
- type: git
|
||||||
url: https://github.com/godbus/dbus
|
url: https://github.com/godbus/dbus
|
||||||
tag: v5.1.0
|
tag: v5.2.2
|
||||||
commit: e523abc905595cf17fb0001a7d77eaaddfaa216d
|
commit: a8ac15ba63645f02ffd57f4b443203279ab40b30
|
||||||
dest: vendor/github.com/godbus/dbus/v5
|
dest: vendor/github.com/godbus/dbus/v5
|
||||||
- type: git
|
- type: git
|
||||||
url: https://github.com/skip2/go-qrcode
|
url: https://github.com/skip2/go-qrcode
|
||||||
|
|
@ -28,9 +26,17 @@ modules:
|
||||||
dest: vendor/github.com/skip2/go-qrcode
|
dest: vendor/github.com/skip2/go-qrcode
|
||||||
- type: git
|
- type: git
|
||||||
url: https://github.com/golang/net
|
url: https://github.com/golang/net
|
||||||
tag: v0.20.0
|
tag: v0.57.0
|
||||||
commit: cb5b10f0bbc51089bf49030ce3bd43bbfee08c23
|
commit: b8f09f6f062ceb4531b7af4bd17a5c8fe9c4b2b5
|
||||||
dest: vendor/golang.org/x/net
|
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
|
buildsystem: simple
|
||||||
build-commands:
|
build-commands:
|
||||||
- . /usr/lib/sdk/golang/enable.sh; go build -tags portal,x11
|
- . /usr/lib/sdk/golang/enable.sh; go build -tags portal,x11
|
||||||
|
|
|
||||||
17
flatpak/modules.txt
Normal file
17
flatpak/modules.txt
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
# github.com/bendahl/uinput v1.7.0
|
||||||
|
## explicit; go 1.13
|
||||||
|
github.com/bendahl/uinput
|
||||||
|
# github.com/godbus/dbus/v5 v5.2.2
|
||||||
|
## explicit; go 1.20
|
||||||
|
github.com/godbus/dbus/v5
|
||||||
|
# github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||||
|
## explicit; go 1.13
|
||||||
|
github.com/skip2/go-qrcode
|
||||||
|
github.com/skip2/go-qrcode/bitset
|
||||||
|
github.com/skip2/go-qrcode/reedsolomon
|
||||||
|
# golang.org/x/net v0.57.0
|
||||||
|
## explicit; go 1.25.0
|
||||||
|
golang.org/x/net/websocket
|
||||||
|
# golang.org/x/sys v0.47.0
|
||||||
|
## explicit; go 1.25.0
|
||||||
|
golang.org/x/sys/unix
|
||||||
8
go.mod
8
go.mod
|
|
@ -1,10 +1,12 @@
|
||||||
module github.com/unrud/remote-touchpad
|
module github.com/unrud/remote-touchpad
|
||||||
|
|
||||||
go 1.20
|
go 1.25.5
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/bendahl/uinput v1.7.0
|
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
|
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||||
golang.org/x/net v0.20.0
|
golang.org/x/net v0.57.0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
require golang.org/x/sys v0.47.0 // indirect
|
||||||
|
|
|
||||||
10
go.sum
10
go.sum
|
|
@ -1,8 +1,10 @@
|
||||||
github.com/bendahl/uinput v1.7.0 h1:nA4fm8Wu8UYNOPykIZm66nkWEyvxzfmJ8YC02PM40jg=
|
github.com/bendahl/uinput v1.7.0 h1:nA4fm8Wu8UYNOPykIZm66nkWEyvxzfmJ8YC02PM40jg=
|
||||||
github.com/bendahl/uinput v1.7.0/go.mod h1:Np7w3DINc9wB83p12fTAM3DPPhFnAKP0WTXRqCQJ6Z8=
|
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.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
|
||||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
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 h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
||||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
|
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
|
||||||
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
|
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
|
||||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
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=
|
||||||
|
|
|
||||||
3
host.go
3
host.go
|
|
@ -74,7 +74,8 @@ func findDefaultHost() string {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for _, linkLocalPrefix := range []string{
|
for _, linkLocalPrefix := range []string{
|
||||||
"169.254.", "fe8", "fe9", "fea", "feb"} {
|
"169.254.", "fe8", "fe9", "fea", "feb",
|
||||||
|
} {
|
||||||
if strings.HasPrefix(ip.String(), linkLocalPrefix) {
|
if strings.HasPrefix(ip.String(), linkLocalPrefix) {
|
||||||
addIP(20, ip)
|
addIP(20, ip)
|
||||||
continue addrs
|
continue addrs
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,10 @@ package inputcontrol
|
||||||
|
|
||||||
import "sort"
|
import "sort"
|
||||||
|
|
||||||
type PointerButton int
|
type (
|
||||||
type Key int
|
PointerButton int
|
||||||
|
Key int
|
||||||
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
PointerButtonLeft PointerButton = iota
|
PointerButtonLeft PointerButton = iota
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,19 @@
|
||||||
package inputcontrol
|
package inputcontrol
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"crypto/hkdf"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/sha256"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"slices"
|
||||||
|
|
||||||
"github.com/godbus/dbus/v5"
|
"github.com/godbus/dbus/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -34,6 +45,8 @@ const (
|
||||||
btnReleased uint32 = 0
|
btnReleased uint32 = 0
|
||||||
btnPressed uint32 = 1
|
btnPressed uint32 = 1
|
||||||
|
|
||||||
|
untilRevoked uint32 = 2
|
||||||
|
|
||||||
// linux/input-event-codes.h
|
// linux/input-event-codes.h
|
||||||
btnLeft int32 = 0x110
|
btnLeft int32 = 0x110
|
||||||
btnRight int32 = 0x111
|
btnRight int32 = 0x111
|
||||||
|
|
@ -42,7 +55,7 @@ const (
|
||||||
|
|
||||||
type portalController struct {
|
type portalController struct {
|
||||||
bus *dbus.Conn
|
bus *dbus.Conn
|
||||||
remoteDesktop dbus.BusObject
|
portalDesktop dbus.BusObject
|
||||||
sessionHandle dbus.ObjectPath
|
sessionHandle dbus.ObjectPath
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -69,85 +82,209 @@ func InitPortalController() (Controller, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, &UnsupportedPlatformError{err}
|
return nil, &UnsupportedPlatformError{err}
|
||||||
}
|
}
|
||||||
remoteDesktop := bus.Object("org.freedesktop.portal.Desktop",
|
portalDesktop := bus.Object("org.freedesktop.portal.Desktop",
|
||||||
"/org/freedesktop/portal/desktop")
|
"/org/freedesktop/portal/desktop")
|
||||||
availableDeviceTypesV, err := remoteDesktop.GetProperty(
|
remoteDesktopVersionV, err := portalDesktop.GetProperty(
|
||||||
|
"org.freedesktop.portal.RemoteDesktop.version")
|
||||||
|
if err != nil {
|
||||||
|
return nil, &UnsupportedPlatformError{
|
||||||
|
fmt.Errorf("getting 'version' failed: %w", err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
remoteDesktopVersion, ok := remoteDesktopVersionV.Value().(uint32)
|
||||||
|
if !ok {
|
||||||
|
return nil, &UnsupportedPlatformError{
|
||||||
|
errors.New("unexpected 'version' type"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
restoreTokenStore, err := func() (*secretStore, error) {
|
||||||
|
if remoteDesktopVersion < 2 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
cacheDirectory, err := os.UserCacheDir()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(cacheDirectory, 0o700); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
secret, err := retrieveSecret(bus)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return newSecretStore(secret,
|
||||||
|
filepath.Join(cacheDirectory, "remote-touchpad.portal-restore-token.bin"))
|
||||||
|
}()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Skipping restore token: %v", err)
|
||||||
|
}
|
||||||
|
availableDeviceTypesV, err := portalDesktop.GetProperty(
|
||||||
"org.freedesktop.portal.RemoteDesktop.AvailableDeviceTypes")
|
"org.freedesktop.portal.RemoteDesktop.AvailableDeviceTypes")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, &UnsupportedPlatformError{err}
|
return nil, &UnsupportedPlatformError{
|
||||||
|
fmt.Errorf("getting 'AvailableDeviceTypes' failed: %w", err),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
availableDeviceTypes, ok := availableDeviceTypesV.Value().(uint32)
|
availableDeviceTypes, ok := availableDeviceTypesV.Value().(uint32)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, &UnsupportedPlatformError{
|
return nil, &UnsupportedPlatformError{
|
||||||
errors.New("unexpected 'AvailableDeviceTypes' return type")}
|
errors.New("unexpected 'AvailableDeviceTypes' return type"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if availableDeviceTypes&deviceKeyboard == 0 ||
|
if availableDeviceTypes&deviceKeyboard == 0 ||
|
||||||
availableDeviceTypes&devicePointer == 0 {
|
availableDeviceTypes&devicePointer == 0 {
|
||||||
return nil, &UnsupportedPlatformError{
|
return nil, &UnsupportedPlatformError{
|
||||||
errors.New("keyboard or pointer source type not supported")}
|
errors.New("keyboard or pointer source type not supported"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
inVardict := make(map[string]dbus.Variant)
|
createSessionResults, err := checkResponse(getResponse(bus, portalDesktop,
|
||||||
inVardict["session_handle_token"] = dbus.MakeVariant("t")
|
"org.freedesktop.portal.RemoteDesktop.CreateSession", 0,
|
||||||
result, outVardict, err := getResponse(bus, remoteDesktop,
|
map[string]dbus.Variant{"session_handle_token": dbus.MakeVariant("t")},
|
||||||
"org.freedesktop.portal.RemoteDesktop.CreateSession", 0, inVardict)
|
))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, &UnsupportedPlatformError{err}
|
|
||||||
}
|
|
||||||
if result != 0 {
|
|
||||||
return nil, &UnsupportedPlatformError{
|
return nil, &UnsupportedPlatformError{
|
||||||
fmt.Errorf("Calling 'CreateSession' failed (%v)", result)}
|
fmt.Errorf("calling 'CreateSession' failed: %w", err),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
sessionHandleV, ok := outVardict["session_handle"]
|
sessionHandleString, ok := createSessionResults["session_handle"].Value().(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, &UnsupportedPlatformError{
|
return nil, &UnsupportedPlatformError{
|
||||||
errors.New("'session_handle' missing from 'CreateSession' return value")}
|
errors.New("unexpected 'session_handle' type in 'CreateSession' return value"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
sessionHandleS, ok := sessionHandleV.Value().(string)
|
sessionHandle := dbus.ObjectPath(sessionHandleString)
|
||||||
if !ok {
|
selectDevicesOptions := map[string]dbus.Variant{
|
||||||
return nil, &UnsupportedPlatformError{
|
"types": dbus.MakeVariant(deviceKeyboard | devicePointer),
|
||||||
errors.New("unexpected 'session_handle' type in 'CreateSession' return value")}
|
|
||||||
}
|
}
|
||||||
sessionHandle := dbus.ObjectPath(sessionHandleS)
|
if restoreTokenStore != nil {
|
||||||
inVardict = make(map[string]dbus.Variant)
|
if restoreToken, err := restoreTokenStore.Load(); err != nil {
|
||||||
inVardict["types"] = dbus.MakeVariant(deviceKeyboard | devicePointer)
|
if !errors.Is(err, os.ErrNotExist) {
|
||||||
result, outVardict, err = getResponse(bus, remoteDesktop,
|
log.Printf("Failed to load restore token: %v", err)
|
||||||
"org.freedesktop.portal.RemoteDesktop.SelectDevices", 0, sessionHandle, inVardict)
|
}
|
||||||
|
} 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 {
|
if err != nil {
|
||||||
return nil, &UnsupportedPlatformError{err}
|
|
||||||
}
|
|
||||||
if result != 0 {
|
|
||||||
return nil, &UnsupportedPlatformError{
|
return nil, &UnsupportedPlatformError{
|
||||||
fmt.Errorf("Calling 'SelectDevices' failed (%v)", result)}
|
fmt.Errorf("calling 'SelectDevices' failed: %w", err),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
inVardict = make(map[string]dbus.Variant)
|
startResponseStatus, startResults, err := getResponse(bus, portalDesktop,
|
||||||
result, outVardict, err = getResponse(bus, remoteDesktop,
|
"org.freedesktop.portal.RemoteDesktop.Start", 0,
|
||||||
"org.freedesktop.portal.RemoteDesktop.Start", 0, sessionHandle, "", inVardict)
|
sessionHandle, "", map[string]dbus.Variant{},
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, &UnsupportedPlatformError{err}
|
return nil, &UnsupportedPlatformError{
|
||||||
|
fmt.Errorf("calling 'Start' failed: %w", err),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if result != 0 {
|
if startResponseStatus != 0 {
|
||||||
return nil, errors.New("keyboard or pointer access denied")
|
return nil, errors.New("keyboard or pointer access denied")
|
||||||
}
|
}
|
||||||
devicesV, ok := outVardict["devices"]
|
if restoreToken, _ := startResults["restore_token"].Value().(string); restoreTokenStore != nil {
|
||||||
if !ok {
|
if err := restoreTokenStore.Store([]byte(restoreToken)); err != nil {
|
||||||
return nil, &UnsupportedPlatformError{
|
log.Printf("Failed to store restore token: %v", err)
|
||||||
errors.New("'devices' missing from 'Start' return value")}
|
}
|
||||||
}
|
}
|
||||||
devices, ok := devicesV.Value().(uint32)
|
devices, ok := startResults["devices"].Value().(uint32)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, &UnsupportedPlatformError{
|
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 {
|
if devices&deviceKeyboard == 0 || devices&devicePointer == 0 {
|
||||||
return nil, errors.New("keyboard or pointer access denied")
|
return nil, errors.New("keyboard or pointer access denied")
|
||||||
}
|
}
|
||||||
cleanupBus = false
|
cleanupBus = false
|
||||||
return &portalController{bus: bus, remoteDesktop: remoteDesktop,
|
return &portalController{
|
||||||
sessionHandle: sessionHandle}, nil
|
bus: bus, portalDesktop: portalDesktop,
|
||||||
|
sessionHandle: sessionHandle,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func retrieveSecret(bus *dbus.Conn) ([]byte, error) {
|
||||||
|
portalDesktop := bus.Object("org.freedesktop.portal.Desktop",
|
||||||
|
"/org/freedesktop/portal/desktop")
|
||||||
|
secretReader, secretWriter, err := os.Pipe()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer secretReader.Close()
|
||||||
|
defer secretWriter.Close()
|
||||||
|
if _, err := checkResponse(getResponse(bus, portalDesktop,
|
||||||
|
"org.freedesktop.portal.Secret.RetrieveSecret", 0,
|
||||||
|
dbus.UnixFD(secretWriter.Fd()), map[string]dbus.Variant{},
|
||||||
|
)); err != nil {
|
||||||
|
return nil, fmt.Errorf("calling 'RetrieveSecret' failed: %w", err)
|
||||||
|
}
|
||||||
|
if err := secretWriter.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
secret, err := io.ReadAll(secretReader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(secret) < 16 {
|
||||||
|
return nil, fmt.Errorf("'RetrieveSecret' returned too few bytes (%d)", len(secret))
|
||||||
|
}
|
||||||
|
return secret, err
|
||||||
|
}
|
||||||
|
|
||||||
|
type secretStore struct {
|
||||||
|
aesgcm cipher.AEAD
|
||||||
|
filename string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSecretStore(key []byte, filename string) (*secretStore, error) {
|
||||||
|
derivedKey, err := hkdf.Key(sha256.New, key, nil, "", 32)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
block, err := aes.NewCipher(derivedKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
aesgcm, err := cipher.NewGCM(block)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &secretStore{
|
||||||
|
aesgcm: aesgcm,
|
||||||
|
filename: filename,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *secretStore) Load() ([]byte, error) {
|
||||||
|
data, err := os.ReadFile(s.filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(data) < s.aesgcm.NonceSize() {
|
||||||
|
return nil, errors.New("invalid ciphertext")
|
||||||
|
}
|
||||||
|
nonce := data[:s.aesgcm.NonceSize()]
|
||||||
|
ciphertext := data[len(nonce):]
|
||||||
|
return s.aesgcm.Open(nil, nonce, ciphertext, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *secretStore) Store(data []byte) error {
|
||||||
|
nonce := make([]byte, s.aesgcm.NonceSize())
|
||||||
|
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ciphertext := s.aesgcm.Seal(nil, nonce, data, nil)
|
||||||
|
return os.WriteFile(s.filename, slices.Concat(nonce, ciphertext), 0o600)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getResponse(bus *dbus.Conn, object dbus.BusObject, method string,
|
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)
|
ch := make(chan *dbus.Signal, 512)
|
||||||
bus.Signal(ch)
|
bus.Signal(ch)
|
||||||
defer bus.RemoveSignal(ch)
|
defer bus.RemoveSignal(ch)
|
||||||
|
|
@ -159,32 +296,39 @@ func getResponse(bus *dbus.Conn, object dbus.BusObject, method string,
|
||||||
s := <-ch
|
s := <-ch
|
||||||
if s.Path == requestPath && s.Name == "org.freedesktop.portal.Request.Response" {
|
if s.Path == requestPath && s.Name == "org.freedesktop.portal.Request.Response" {
|
||||||
if len(s.Body) != 2 {
|
if len(s.Body) != 2 {
|
||||||
return 0, nil, errors.New("unexpected 'Response' return length")
|
return 0, nil, fmt.Errorf("unexpected 'Response' return length (%d)", len(s.Body))
|
||||||
}
|
}
|
||||||
result, ok := s.Body[0].(uint32)
|
responseStatus, ok := s.Body[0].(uint32)
|
||||||
if !ok {
|
if !ok {
|
||||||
return 0, nil, errors.New("unexpected 'Response' return type")
|
return 0, nil, errors.New("unexpected 'Response' return type")
|
||||||
}
|
}
|
||||||
outVardict, ok := s.Body[1].(map[string]dbus.Variant)
|
results, ok := s.Body[1].(map[string]dbus.Variant)
|
||||||
if !ok {
|
if !ok {
|
||||||
return 0, nil, errors.New("unexpected 'Response' return type")
|
return 0, nil, errors.New("unexpected 'Response' return type")
|
||||||
}
|
}
|
||||||
return result, outVardict, nil
|
return responseStatus, results, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func checkResponse(responseStatus uint32, results map[string]dbus.Variant, err error) (map[string]dbus.Variant, error) {
|
||||||
|
if err == nil && responseStatus != 0 {
|
||||||
|
err = fmt.Errorf("unexpected 'Response' status (%d)", responseStatus)
|
||||||
|
}
|
||||||
|
return results, err
|
||||||
|
}
|
||||||
|
|
||||||
func (p *portalController) Close() error {
|
func (p *portalController) Close() error {
|
||||||
return p.bus.Close()
|
return p.bus.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *portalController) keyboardKeys(keys []Keysym) error {
|
func (p *portalController) keyboardKeys(keys []Keysym) error {
|
||||||
inVardict := make(map[string]dbus.Variant)
|
|
||||||
for _, keysym := range keys {
|
for _, keysym := range keys {
|
||||||
for _, state := range [...]uint32{btnPressed, btnReleased} {
|
for _, state := range [...]uint32{btnPressed, btnReleased} {
|
||||||
if err := p.remoteDesktop.Call(
|
if err := p.portalDesktop.Call(
|
||||||
"org.freedesktop.portal.RemoteDesktop.NotifyKeyboardKeysym",
|
"org.freedesktop.portal.RemoteDesktop.NotifyKeyboardKeysym", 0,
|
||||||
0, p.sessionHandle, inVardict, keysym, state).Store(); err != nil {
|
p.sessionHandle, map[string]dbus.Variant{}, keysym, state,
|
||||||
|
).Store(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -229,28 +373,27 @@ func (p *portalController) PointerButton(button PointerButton, press bool) error
|
||||||
if press {
|
if press {
|
||||||
state = btnPressed
|
state = btnPressed
|
||||||
}
|
}
|
||||||
inVardict := make(map[string]dbus.Variant)
|
if err := p.portalDesktop.Call("org.freedesktop.portal.RemoteDesktop.NotifyPointerButton", 0,
|
||||||
if err := p.remoteDesktop.Call("org.freedesktop.portal.RemoteDesktop.NotifyPointerButton",
|
p.sessionHandle, map[string]dbus.Variant{}, btn, state,
|
||||||
0, p.sessionHandle, inVardict, btn, state).Store(); err != nil {
|
).Store(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *portalController) PointerMove(deltaX, deltaY int) error {
|
func (p *portalController) PointerMove(deltaX, deltaY int) error {
|
||||||
inVardict := make(map[string]dbus.Variant)
|
if err := p.portalDesktop.Call("org.freedesktop.portal.RemoteDesktop.NotifyPointerMotion", 0,
|
||||||
if err := p.remoteDesktop.Call("org.freedesktop.portal.RemoteDesktop.NotifyPointerMotion",
|
p.sessionHandle, map[string]dbus.Variant{}, float64(deltaX), float64(deltaY),
|
||||||
0, p.sessionHandle, inVardict, float64(deltaX), float64(deltaY)).Store(); err != nil {
|
).Store(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *portalController) PointerScroll(deltaHorizontal, deltaVertical int, finish bool) error {
|
func (p *portalController) PointerScroll(deltaHorizontal, deltaVertical int, finish bool) error {
|
||||||
inVardict := make(map[string]dbus.Variant)
|
if err := p.portalDesktop.Call("org.freedesktop.portal.RemoteDesktop.NotifyPointerAxis", 0,
|
||||||
inVardict["finish"] = dbus.MakeVariant(finish)
|
p.sessionHandle, map[string]dbus.Variant{"finish": dbus.MakeVariant(finish)}, float64(deltaHorizontal), float64(deltaVertical),
|
||||||
if err := p.remoteDesktop.Call("org.freedesktop.portal.RemoteDesktop.NotifyPointerAxis",
|
).Store(); err != nil {
|
||||||
0, p.sessionHandle, inVardict, float64(deltaHorizontal), float64(deltaVertical)).Store(); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -25,10 +25,11 @@ package inputcontrol
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/bendahl/uinput"
|
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/bendahl/uinput"
|
||||||
)
|
)
|
||||||
|
|
||||||
const shiftKeysDelay time.Duration = 50 * time.Millisecond
|
const shiftKeysDelay time.Duration = 50 * time.Millisecond
|
||||||
|
|
|
||||||
|
|
@ -32,9 +32,12 @@ package inputcontrol
|
||||||
// return DefaultRootWindow(dpy);
|
// return DefaultRootWindow(dpy);
|
||||||
// }
|
// }
|
||||||
import "C"
|
import "C"
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"maps"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -42,12 +45,18 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
keyboardMappingDelay time.Duration = 500 * time.Millisecond
|
keyboardMappingDelay time.Duration = 200 * time.Millisecond
|
||||||
scrollDiv int = 20
|
scrollDiv int = 20
|
||||||
)
|
)
|
||||||
|
|
||||||
var modifierIndices [6]uint = [...]uint{C.ShiftMapIndex, C.Mod1MapIndex,
|
var modifierIndexes = [...]int{
|
||||||
C.Mod2MapIndex, C.Mod3MapIndex, C.Mod4MapIndex, C.Mod5MapIndex}
|
C.ShiftMapIndex,
|
||||||
|
C.Mod1MapIndex,
|
||||||
|
C.Mod2MapIndex,
|
||||||
|
C.Mod3MapIndex,
|
||||||
|
C.Mod4MapIndex,
|
||||||
|
C.Mod5MapIndex,
|
||||||
|
}
|
||||||
|
|
||||||
type x11Controller struct {
|
type x11Controller struct {
|
||||||
display *C.Display
|
display *C.Display
|
||||||
|
|
@ -63,13 +72,15 @@ func InitX11Controller() (Controller, error) {
|
||||||
display := C.XOpenDisplay(nil)
|
display := C.XOpenDisplay(nil)
|
||||||
if display == nil {
|
if display == nil {
|
||||||
return nil, &UnsupportedPlatformError{
|
return nil, &UnsupportedPlatformError{
|
||||||
errors.New("failed to connect to X server")}
|
errors.New("failed to connect to X server"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
p := &x11Controller{display: display}
|
p := &x11Controller{display: display}
|
||||||
if p.xIsXwayland() {
|
if p.xIsXwayland() {
|
||||||
p.Close()
|
p.Close()
|
||||||
return nil, &UnsupportedPlatformError{
|
return nil, &UnsupportedPlatformError{
|
||||||
errors.New("X server is Xwayland")}
|
errors.New("X server is Xwayland"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return p, nil
|
return p, nil
|
||||||
}
|
}
|
||||||
|
|
@ -113,53 +124,68 @@ func (p *x11Controller) Close() error {
|
||||||
return nil
|
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
|
var minKeycodes, maxKeycodes C.int
|
||||||
C.XDisplayKeycodes(p.display, &minKeycodes, &maxKeycodes)
|
C.XDisplayKeycodes(p.display, &minKeycodes, &maxKeycodes)
|
||||||
var keysymsPerKeycode C.int
|
var keysymsPerKeycodeC C.int
|
||||||
keysyms := C.XGetKeyboardMapping(p.display, C.KeyCode(minKeycodes),
|
keysyms := C.XGetKeyboardMapping(
|
||||||
maxKeycodes-minKeycodes+1, &keysymsPerKeycode)
|
p.display,
|
||||||
|
C.KeyCode(minKeycodes),
|
||||||
|
maxKeycodes-minKeycodes+1,
|
||||||
|
&keysymsPerKeycodeC,
|
||||||
|
)
|
||||||
if keysyms == nil {
|
if keysyms == nil {
|
||||||
return 0, 0, errors.New("failed to get keyboard mapping")
|
return 0, 0, errors.New("failed to get keyboard mapping")
|
||||||
}
|
}
|
||||||
defer C.XFree(unsafe.Pointer(keysyms))
|
defer C.XFree(unsafe.Pointer(keysyms))
|
||||||
|
keysymsPerKeycode = int(keysymsPerKeycodeC)
|
||||||
keycodes:
|
keycodes:
|
||||||
for keycode := C.KeyCode(minKeycodes); keycode <= C.KeyCode(maxKeycodes); keycode++ {
|
for keycode := int(minKeycodes); keycode <= int(maxKeycodes); keycode++ {
|
||||||
for i := 0; i < int(keysymsPerKeycode); i++ {
|
for i := range keysymsPerKeycode {
|
||||||
keysymsIndex := int(keycode-
|
keysymsIndex := (keycode-int(minKeycodes))*keysymsPerKeycode + i
|
||||||
C.KeyCode(minKeycodes))*int(keysymsPerKeycode) + i
|
keysym := *(*C.KeySym)(unsafe.Pointer(
|
||||||
keysym := *(*C.KeySym)(unsafe.Pointer(uintptr(unsafe.Pointer(keysyms)) +
|
uintptr(unsafe.Pointer(keysyms)) +
|
||||||
uintptr(keysymsIndex)*unsafe.Sizeof(*keysyms)))
|
uintptr(keysymsIndex)*unsafe.Sizeof(*keysyms),
|
||||||
|
))
|
||||||
if keysym != 0 {
|
if keysym != 0 {
|
||||||
continue keycodes
|
continue keycodes
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return keycode, keysymsPerKeycode, nil
|
return C.KeyCode(keycode), keysymsPerKeycode, nil
|
||||||
}
|
}
|
||||||
return 0, 0, errors.New("no empty keycode found")
|
return 0, 0, errors.New("no empty keycode found")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *x11Controller) changeKeyMappingLocked(keysymsPerKeycode C.int,
|
func (p *x11Controller) changeKeyMappingLocked(
|
||||||
keycode C.KeyCode, keysym Keysym) {
|
keycode C.KeyCode, keysymsPerKeycode int, keysym Keysym,
|
||||||
|
) {
|
||||||
keycodeMapping := make([]C.KeySym, keysymsPerKeycode)
|
keycodeMapping := make([]C.KeySym, keysymsPerKeycode)
|
||||||
for i := range keycodeMapping {
|
for i := range keycodeMapping {
|
||||||
keycodeMapping[i] = C.KeySym(keysym)
|
keycodeMapping[i] = C.KeySym(keysym)
|
||||||
}
|
}
|
||||||
C.XChangeKeyboardMapping(p.display, C.int(keycode), keysymsPerKeycode,
|
C.XChangeKeyboardMapping(
|
||||||
(*C.KeySym)(unsafe.Pointer(&keycodeMapping[0])), 1)
|
p.display,
|
||||||
C.XFlush(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 {
|
func (p *x11Controller) getModKeycodesLocked() map[uint]C.KeyCode {
|
||||||
modKeymap := C.XGetModifierMapping(p.display)
|
modKeymap := C.XGetModifierMapping(p.display)
|
||||||
defer C.XFreeModifiermap(modKeymap)
|
defer C.XFreeModifiermap(modKeymap)
|
||||||
modKeycodes := make(map[uint]C.KeyCode)
|
modKeycodes := make(map[uint]C.KeyCode)
|
||||||
for _, modIndex := range modifierIndices {
|
for _, modIndex := range modifierIndexes {
|
||||||
for i := 0; i < int(modKeymap.max_keypermod); i++ {
|
for i := range int(modKeymap.max_keypermod) {
|
||||||
keycode := *(*C.KeyCode)(unsafe.Pointer(uintptr(unsafe.Pointer(modKeymap.modifiermap)) +
|
keycodeIndex := modIndex*int(modKeymap.max_keypermod) + i
|
||||||
uintptr(uint(modIndex)*uint(modKeymap.max_keypermod)+uint(i))))
|
keycode := *(*C.KeyCode)(unsafe.Pointer(
|
||||||
|
uintptr(unsafe.Pointer(modKeymap.modifiermap)) +
|
||||||
|
uintptr(keycodeIndex)*unsafe.Sizeof(*modKeymap.modifiermap),
|
||||||
|
))
|
||||||
if keycode != 0 {
|
if keycode != 0 {
|
||||||
modKeycodes[1<<uint(modIndex)] = keycode
|
modKeycodes[1<<modIndex] = keycode
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -167,50 +193,39 @@ func (p *x11Controller) getModKeycodesLocked() map[uint]C.KeyCode {
|
||||||
return modKeycodes
|
return modKeycodes
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *x11Controller) findKeycodeLocked(keyboard C.XkbDescPtr,
|
func (p *x11Controller) findKeycodeLocked(
|
||||||
modKeycodes map[uint]C.KeyCode, activeMods C.uint,
|
keyboard C.XkbDescPtr, modKeycodes map[uint]C.KeyCode, activeMods uint, keysym Keysym,
|
||||||
keysym Keysym) (C.KeyCode, C.uint) {
|
) (keycode C.KeyCode, mods uint) {
|
||||||
keycode := C.XKeysymToKeycode(p.display, C.KeySym(keysym))
|
keycode = C.XKeysymToKeycode(p.display, C.KeySym(keysym))
|
||||||
if keycode == 0 {
|
if keycode == 0 {
|
||||||
return 0, 0
|
return 0, 0
|
||||||
}
|
}
|
||||||
var alwaysActiveMods C.uint
|
for activeModBitmap := range uint(1) << len(modKeycodes) {
|
||||||
for modIndex := uint(0); modIndex < 8; modIndex++ {
|
mods := activeMods
|
||||||
mod := uint(1) << modIndex
|
for i, mod := range slices.Sorted(maps.Keys(modKeycodes)) {
|
||||||
if _, modAvailable := modKeycodes[mod]; !modAvailable {
|
if activeModBitmap&(1<<i) != 0 {
|
||||||
alwaysActiveMods |= activeMods & C.uint(mod)
|
mods ^= mod
|
||||||
}
|
|
||||||
}
|
|
||||||
_, shiftModAvailable := modKeycodes[C.ShiftMask]
|
|
||||||
for _, modIndex := range modifierIndices {
|
|
||||||
var mod C.uint
|
|
||||||
if modIndex != C.ShiftMapIndex {
|
|
||||||
mod = 1 << modIndex
|
|
||||||
}
|
|
||||||
for _, shiftMod := range [...]C.uint{0, C.ShiftMask} {
|
|
||||||
if shiftMod != 0 && !shiftModAvailable {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
mods := alwaysActiveMods | shiftMod | mod
|
|
||||||
var retMods C.uint
|
|
||||||
var retKeysym C.KeySym
|
|
||||||
C.XkbTranslateKeyCode(keyboard, keycode, mods, &retMods, &retKeysym)
|
|
||||||
if retKeysym == C.KeySym(keysym) {
|
|
||||||
return keycode, mods
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
var retMods C.uint
|
||||||
|
var retKeysym C.KeySym
|
||||||
|
C.XkbTranslateKeyCode(keyboard, keycode, C.uint(mods), &retMods, &retKeysym)
|
||||||
|
if retKeysym == C.KeySym(keysym) {
|
||||||
|
return keycode, mods
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return 0, 0
|
return 0, 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *x11Controller) sendModsLocked(modKeycodes map[uint]C.KeyCode, mods C.uint,
|
func (p *x11Controller) sendModsLocked(
|
||||||
press bool) {
|
modKeycodes map[uint]C.KeyCode, mods uint, press bool,
|
||||||
|
) {
|
||||||
var pressC C.int = C.False
|
var pressC C.int = C.False
|
||||||
if press {
|
if press {
|
||||||
pressC = C.True
|
pressC = C.True
|
||||||
}
|
}
|
||||||
for mod, keycode := range modKeycodes {
|
for mod, keycode := range modKeycodes {
|
||||||
if mods&C.uint(mod) != 0 {
|
if mods&mod != 0 {
|
||||||
C.XTestFakeKeyEvent(p.display, C.uint(keycode), pressC, 0)
|
C.XTestFakeKeyEvent(p.display, C.uint(keycode), pressC, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -231,17 +246,22 @@ func (p *x11Controller) keyboardKeys(keys []Keysym) error {
|
||||||
C.XkbCompatMapMask|C.XkbGeometryMask, C.XkbUseCoreKbd)
|
C.XkbCompatMapMask|C.XkbGeometryMask, C.XkbUseCoreKbd)
|
||||||
defer C.XkbFreeKeyboard(keyboard, C.XkbAllComponentsMask, C.True)
|
defer C.XkbFreeKeyboard(keyboard, C.XkbAllComponentsMask, C.True)
|
||||||
var emptyKeycode C.KeyCode
|
var emptyKeycode C.KeyCode
|
||||||
var keysymsPerKeycode C.int
|
var keysymsPerKeycode int
|
||||||
for _, keysym := range keys {
|
for _, keysym := range keys {
|
||||||
var root, child C.Window
|
var root, child C.Window
|
||||||
var rootX, rootY, x, y C.int
|
var rootX, rootY, x, y C.int
|
||||||
var activeMods C.uint
|
var activeModsC C.uint
|
||||||
C.XSync(p.display, C.False)
|
C.XQueryPointer(
|
||||||
C.XQueryPointer(p.display, rootWindow, &root, &child, &rootX, &rootY,
|
p.display,
|
||||||
&x, &y, &activeMods)
|
rootWindow,
|
||||||
keycode, mods := p.findKeycodeLocked(keyboard, modKeycodes, activeMods,
|
&root, &child,
|
||||||
keysym)
|
&rootX, &rootY,
|
||||||
var pressMods, releaseMods C.uint
|
&x, &y,
|
||||||
|
&activeModsC,
|
||||||
|
)
|
||||||
|
activeMods := uint(activeModsC)
|
||||||
|
keycode, mods := p.findKeycodeLocked(keyboard, modKeycodes, activeMods, keysym)
|
||||||
|
var pressMods, releaseMods uint
|
||||||
if keycode == 0 {
|
if keycode == 0 {
|
||||||
if emptyKeycode == 0 {
|
if emptyKeycode == 0 {
|
||||||
var err error
|
var err error
|
||||||
|
|
@ -249,12 +269,10 @@ func (p *x11Controller) keyboardKeys(keys []Keysym) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer p.changeKeyMappingLocked(keysymsPerKeycode, emptyKeycode, 0)
|
defer p.changeKeyMappingLocked(emptyKeycode, keysymsPerKeycode, 0)
|
||||||
}
|
}
|
||||||
keycode = emptyKeycode
|
keycode = emptyKeycode
|
||||||
p.changeKeyMappingLocked(keysymsPerKeycode, keycode, keysym)
|
p.changeKeyMappingLocked(keycode, keysymsPerKeycode, keysym)
|
||||||
// race condition!
|
|
||||||
time.Sleep(keyboardMappingDelay)
|
|
||||||
} else {
|
} else {
|
||||||
pressMods = mods & ^activeMods
|
pressMods = mods & ^activeMods
|
||||||
releaseMods = activeMods & ^mods
|
releaseMods = activeMods & ^mods
|
||||||
|
|
@ -265,7 +283,7 @@ func (p *x11Controller) keyboardKeys(keys []Keysym) error {
|
||||||
C.XTestFakeKeyEvent(p.display, C.uint(keycode), C.False, 0)
|
C.XTestFakeKeyEvent(p.display, C.uint(keycode), C.False, 0)
|
||||||
p.sendModsLocked(modKeycodes, pressMods, false)
|
p.sendModsLocked(modKeycodes, pressMods, false)
|
||||||
p.sendModsLocked(modKeycodes, releaseMods, true)
|
p.sendModsLocked(modKeycodes, releaseMods, true)
|
||||||
C.XFlush(p.display)
|
C.XSync(p.display, C.False)
|
||||||
if keycode == emptyKeycode {
|
if keycode == emptyKeycode {
|
||||||
// race condition!
|
// race condition!
|
||||||
time.Sleep(keyboardMappingDelay)
|
time.Sleep(keyboardMappingDelay)
|
||||||
|
|
@ -309,7 +327,7 @@ func (p *x11Controller) sendButton(button uint, press bool) error {
|
||||||
pressC = C.True
|
pressC = C.True
|
||||||
}
|
}
|
||||||
C.XTestFakeButtonEvent(p.display, C.uint(button), pressC, 0)
|
C.XTestFakeButtonEvent(p.display, C.uint(button), pressC, 0)
|
||||||
C.XFlush(p.display)
|
C.XSync(p.display, C.False)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -333,7 +351,7 @@ func (p *x11Controller) PointerMove(deltaX, deltaY int) error {
|
||||||
return errors.New("X server connection closed")
|
return errors.New("X server connection closed")
|
||||||
}
|
}
|
||||||
C.XTestFakeRelativeMotionEvent(p.display, C.int(deltaX), C.int(deltaY), 0)
|
C.XTestFakeRelativeMotionEvent(p.display, C.int(deltaX), C.int(deltaY), 0)
|
||||||
C.XFlush(p.display)
|
C.XSync(p.display, C.False)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -354,7 +372,7 @@ func (p *x11Controller) PointerScroll(deltaHorizontal, deltaVertical int, finish
|
||||||
buttonHorizontal = 6
|
buttonHorizontal = 6
|
||||||
stepsHorizontal = -stepsHorizontal
|
stepsHorizontal = -stepsHorizontal
|
||||||
}
|
}
|
||||||
for i := 0; i < stepsHorizontal; i++ {
|
for range stepsHorizontal {
|
||||||
if err := p.sendButton(buttonHorizontal, true); err != nil {
|
if err := p.sendButton(buttonHorizontal, true); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -367,7 +385,7 @@ func (p *x11Controller) PointerScroll(deltaHorizontal, deltaVertical int, finish
|
||||||
buttonVertical = 4
|
buttonVertical = 4
|
||||||
stepsVertical = -stepsVertical
|
stepsVertical = -stepsVertical
|
||||||
}
|
}
|
||||||
for i := 0; i < stepsVertical; i++ {
|
for range stepsVertical {
|
||||||
if err := p.sendButton(buttonVertical, true); err != nil {
|
if err := p.sendButton(buttonVertical, true); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
9
main.go
9
main.go
|
|
@ -27,9 +27,6 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/unrud/remote-touchpad/inputcontrol"
|
|
||||||
"github.com/unrud/remote-touchpad/terminal"
|
|
||||||
"golang.org/x/net/websocket"
|
|
||||||
"log"
|
"log"
|
||||||
mathrand "math/rand"
|
mathrand "math/rand"
|
||||||
"net"
|
"net"
|
||||||
|
|
@ -39,6 +36,10 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"github.com/unrud/remote-touchpad/inputcontrol"
|
||||||
|
"github.com/unrud/remote-touchpad/terminal"
|
||||||
|
"golang.org/x/net/websocket"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
@ -47,7 +48,7 @@ const (
|
||||||
authenticationRateBurst int = 10
|
authenticationRateBurst int = 10
|
||||||
challengeLength int = 8
|
challengeLength int = 8
|
||||||
defaultBind string = ":0"
|
defaultBind string = ":0"
|
||||||
version string = "1.4.5"
|
version string = "1.5.4"
|
||||||
prettyAppName string = "Remote Touchpad"
|
prettyAppName string = "Remote Touchpad"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
12
remote-touchpad.service
Normal file
12
remote-touchpad.service
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
[Unit]
|
||||||
|
Description=Remote Touchpad Service
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=%h/.local/bin/remote-touchpad
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5s
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
149
rts
Executable file
149
rts
Executable file
|
|
@ -0,0 +1,149 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Remote Touchpad CLI Wrapper
|
||||||
|
# Provides easy access to remote-touchpad service
|
||||||
|
|
||||||
|
SERVICE_NAME="remote-touchpad"
|
||||||
|
SERVICE_USER="$USER"
|
||||||
|
|
||||||
|
install() {
|
||||||
|
echo "Installing remote-touchpad as a user systemd service..."
|
||||||
|
|
||||||
|
# Check if Go is installed
|
||||||
|
if ! command -v go &> /dev/null; then
|
||||||
|
echo "Error: Go is not installed. Please install Go first."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Building remote-touchpad binary..."
|
||||||
|
go build -tags portal,uinput,x11 -o remote-touchpad
|
||||||
|
|
||||||
|
if [ ! -f "remote-touchpad" ]; then
|
||||||
|
echo "Error: Failed to build remote-touchpad binary"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Setup directories
|
||||||
|
mkdir -p ~/.local/bin ~/.config/systemd/user
|
||||||
|
|
||||||
|
echo "Installing service files..."
|
||||||
|
cp remote-touchpad ~/.local/bin/ || echo "Error: cp to ~/.local/bin failed"
|
||||||
|
cp remote-touchpad.service ~/.config/systemd/user/ || echo "Error: cp to ~/.config/systemd/user failed"
|
||||||
|
|
||||||
|
echo "Enabling and starting service..."
|
||||||
|
systemctl --user daemon-reload
|
||||||
|
systemctl --user enable --now ${SERVICE_NAME}.service
|
||||||
|
|
||||||
|
echo "✓ remote-touchpad user service installed and started"
|
||||||
|
echo ""
|
||||||
|
echo "Use './rts status' to check the service"
|
||||||
|
echo "Use './rts url' to get the connection URL"
|
||||||
|
}
|
||||||
|
|
||||||
|
start() {
|
||||||
|
systemctl --user start ${SERVICE_NAME}.service
|
||||||
|
echo "Started remote-touchpad service"
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
systemctl --user stop ${SERVICE_NAME}.service
|
||||||
|
echo "Stopped remote-touchpad service"
|
||||||
|
}
|
||||||
|
|
||||||
|
status() {
|
||||||
|
systemctl --user status ${SERVICE_NAME}.service --no-pager
|
||||||
|
}
|
||||||
|
|
||||||
|
restart() {
|
||||||
|
systemctl --user restart ${SERVICE_NAME}.service
|
||||||
|
echo "Restarted remote-touchpad service"
|
||||||
|
}
|
||||||
|
|
||||||
|
url() {
|
||||||
|
# Extract URL from service logs
|
||||||
|
echo "Fetching remote-touchpad URL..."
|
||||||
|
local url=$(journalctl --user -u ${SERVICE_NAME}.service -n 100 --no-pager 2>/dev/null | grep -oE "http://[^ ]+|https://[^ ]+" | tail -1)
|
||||||
|
if [ -n "$url" ]; then
|
||||||
|
echo "Remote Touchpad URL: $url"
|
||||||
|
# Generate QR code if qrencode is available
|
||||||
|
if command -v qrencode &> /dev/null; then
|
||||||
|
echo "Generating QR code..."
|
||||||
|
echo "$url" | qrencode -t ansiutf8
|
||||||
|
else
|
||||||
|
echo "QR code generation requires qrencode. Install it with:"
|
||||||
|
echo " sudo apt-get install qrencode # Debian/Ubuntu"
|
||||||
|
echo " sudo dnf install qrencode # Fedora"
|
||||||
|
echo " brew install qrencode # macOS"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Could not find URL in service logs. Is the service running?"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
log() {
|
||||||
|
echo "Viewing remote-touchpad service logs..."
|
||||||
|
journalctl --user -u ${SERVICE_NAME}.service --no-pager
|
||||||
|
}
|
||||||
|
|
||||||
|
uninstall() {
|
||||||
|
echo "Uninstalling remote-touchpad..."
|
||||||
|
systemctl --user stop ${SERVICE_NAME}.service
|
||||||
|
systemctl --user disable ${SERVICE_NAME}.service
|
||||||
|
rm -f ~/.config/systemd/user/${SERVICE_NAME}.service
|
||||||
|
rm -f ~/.local/bin/remote-touchpad
|
||||||
|
systemctl --user daemon-reload
|
||||||
|
echo "✓ remote-touchpad uninstalled"
|
||||||
|
}
|
||||||
|
|
||||||
|
help() {
|
||||||
|
echo "Remote Touchpad CLI Wrapper"
|
||||||
|
echo ""
|
||||||
|
echo "Usage: rts [command]"
|
||||||
|
echo ""
|
||||||
|
echo "Commands:"
|
||||||
|
echo " install - Install remote-touchpad as a systemd user service"
|
||||||
|
echo " start - Start the remote-touchpad service"
|
||||||
|
echo " stop - Stop the remote-touchpad service"
|
||||||
|
echo " status - Check service status"
|
||||||
|
echo " restart - Restart the service"
|
||||||
|
echo " url - Show the remote-touchpad URL"
|
||||||
|
echo " log - View service logs"
|
||||||
|
echo " uninstall - Uninstall the service"
|
||||||
|
echo " help - Show this help message"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main command handler
|
||||||
|
case "$1" in
|
||||||
|
install)
|
||||||
|
install
|
||||||
|
;;
|
||||||
|
start)
|
||||||
|
start
|
||||||
|
;;
|
||||||
|
stop)
|
||||||
|
stop
|
||||||
|
;;
|
||||||
|
status)
|
||||||
|
status
|
||||||
|
;;
|
||||||
|
restart)
|
||||||
|
restart
|
||||||
|
;;
|
||||||
|
url)
|
||||||
|
url
|
||||||
|
;;
|
||||||
|
log)
|
||||||
|
log
|
||||||
|
;;
|
||||||
|
uninstall)
|
||||||
|
uninstall
|
||||||
|
;;
|
||||||
|
help|--help|-h|"")
|
||||||
|
help
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown command: $1"
|
||||||
|
help
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
@ -1,17 +1,21 @@
|
||||||
# Build by running "snapcraft".
|
# Build by running `snapcraft`.
|
||||||
|
|
||||||
# WARNING:
|
# WARNING:
|
||||||
# Snapcraft uses caching for already build steps but it's buggy and can cause strange problems.
|
# 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
|
name: remote-touchpad
|
||||||
license: GPL-3.0+
|
license: GPL-3.0+
|
||||||
grade: stable
|
grade: stable
|
||||||
adopt-info: remote-touchpad
|
adopt-info: remote-touchpad
|
||||||
|
|
||||||
base: core22
|
base: core24
|
||||||
confinement: strict
|
confinement: strict
|
||||||
|
|
||||||
|
platforms:
|
||||||
|
amd64:
|
||||||
|
arm64:
|
||||||
|
|
||||||
apps:
|
apps:
|
||||||
remote-touchpad:
|
remote-touchpad:
|
||||||
command: bin/remote-touchpad
|
command: bin/remote-touchpad
|
||||||
|
|
@ -48,10 +52,6 @@ parts:
|
||||||
- libxt6
|
- libxt6
|
||||||
- libxrandr2
|
- libxrandr2
|
||||||
- libxtst6
|
- 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: |
|
override-build: |
|
||||||
craftctl default
|
craftctl default
|
||||||
install -Dm0755 -t "${CRAFT_PART_INSTALL}/bin" desktop/remote-touchpad-wait-on-error
|
install -Dm0755 -t "${CRAFT_PART_INSTALL}/bin" desktop/remote-touchpad-wait-on-error
|
||||||
|
|
|
||||||
33
webdata.go
33
webdata.go
|
|
@ -1,19 +1,52 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 Unrud <unrud@outlook.com>
|
||||||
|
*
|
||||||
|
* This file is part of Remote-Touchpad.
|
||||||
|
*
|
||||||
|
* Remote-Touchpad is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Remote-Touchpad is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Remote-Touchpad. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"embed"
|
"embed"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"log"
|
"log"
|
||||||
|
"mime"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed webdata/*
|
//go:embed webdata/*
|
||||||
var webdataFSWithPrefix embed.FS
|
var webdataFSWithPrefix embed.FS
|
||||||
var webdataFS fs.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() {
|
func init() {
|
||||||
var err error
|
var err error
|
||||||
webdataFS, err = fs.Sub(webdataFSWithPrefix, "webdata")
|
webdataFS, err = fs.Sub(webdataFSWithPrefix, "webdata")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
for ext, typ := range webdataTypes {
|
||||||
|
if err := mime.AddExtensionType(ext, typ); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
42
webdata_test.go
Normal file
42
webdata_test.go
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 Unrud <unrud@outlook.com>
|
||||||
|
*
|
||||||
|
* This file is part of Remote-Touchpad.
|
||||||
|
*
|
||||||
|
* Remote-Touchpad is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Remote-Touchpad is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Remote-Touchpad. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestWebdataTypesCompleteness(t *testing.T) {
|
||||||
|
if err := fs.WalkDir(webdataFS, ".", func(_ string, d fs.DirEntry, err error) error {
|
||||||
|
if err != nil || d.IsDir() {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ext := filepath.Ext(d.Name())
|
||||||
|
if _, haveType := webdataTypes[ext]; !haveType {
|
||||||
|
return fmt.Errorf("missing mime type for extension %#v", ext)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue