Compare commits

..

172 commits

Author SHA1 Message Date
2ac00c3b48 Updated gitignore
Some checks failed
Test / test (linux, []) (push) Has been cancelled
Test / test (windows, [null]) (push) Has been cancelled
Test / test (linux, [null portal uinput x11]) (push) Has been cancelled
Test / test (linux, [null]) (push) Has been cancelled
Test / test (linux, [portal]) (push) Has been cancelled
Test / test (linux, [uinput]) (push) Has been cancelled
Test / test (linux, [x11]) (push) Has been cancelled
Test / test (windows, []) (push) Has been cancelled
2026-08-14 22:01:02 +02:00
ed7e8bae54 Add systemd user service and CLI wrapper for remote-touchpad
Some checks are pending
Test / test (linux, []) (push) Waiting to run
Test / test (linux, [null portal uinput x11]) (push) Waiting to run
Test / test (linux, [null]) (push) Waiting to run
Test / test (linux, [portal]) (push) Waiting to run
Test / test (linux, [uinput]) (push) Waiting to run
Test / test (linux, [x11]) (push) Waiting to run
Test / test (windows, []) (push) Waiting to run
Test / test (windows, [null]) (push) Waiting to run
This commit introduces a complete systemd user service setup for
remote-touchpad, eliminating the need for users to manually manage
the binary process. Key additions:

- New rts CLI wrapper script providing a user-friendly interface
  for service management without requiring root access
- Systemd service file for proper background operation with
  restart policies and dependency management
- Service management commands: install, start, stop, status,
  restart, and uninstall
- URL extraction and QR code generation via the url command
- Service log viewing via the log command

The installation process now:
1. Verifies Go is installed
2. Builds the remote-touchpad binary with appropriate tags
3. Installs service files to ~/.config/systemd/user/
4. Installs the binary to ~/.local/bin/
5. Enables and starts the service automatically

The README has been updated with comprehensive installation
instructions for the new user service approach.
2026-08-14 21:59:58 +02:00
Unrud
a77f3aa8ec Bump version to 1.5.4 2026-08-03 00:34:58 +02:00
Unrud
9764d1975d Update dependencies 2026-08-03 00:34:58 +02:00
Unrud
5e68074781 X11: Reduce delay when mapping new keys
Reliability is improved by XSync
2026-08-03 00:32:34 +02:00
Unrud
f80b1d6ef7 X11: Minimize mod changes 2026-08-03 00:32:34 +02:00
Unrud
346d9cf375 X11: Cleanup code 2026-08-03 00:32:33 +02:00
Unrud
6ef2be8776 Format code 2026-08-02 22:12:27 +02:00
Unrud
85b6d6ea9d X11: Use XSync to improve reliability 2026-08-02 13:10:04 +02:00
Unrud
1aa8f92cc8 Use range over integers 2026-08-02 13:10:04 +02:00
Unrud
996edb0501 Snap: Specifiy supported platforms 2026-07-11 20:59:03 +02:00
Unrud
a30bed4624 Snap: Update to core24 2026-07-11 20:20:03 +02:00
Unrud
a2dfa7ac47 Bump version to 1.5.3 2025-12-24 22:59:32 +01:00
Unrud
eebde58826 Update dependencies 2025-12-24 22:57:01 +01:00
Unrud
800171efdd Bump version to 1.5.2 2025-10-12 06:45:35 +02:00
Unrud
2b3d5c5bf3 Flatpak: Update modules.txt 2025-10-12 06:42:46 +02:00
Unrud
1add8e4cb8 Bump version to 1.5.1 2025-10-12 06:26:29 +02:00
Unrud
06674b224f Update dependencies 2025-10-12 06:26:11 +02:00
Unrud
d48d9aed3d Bump version to 1.5.0 2025-03-18 22:02:15 +01:00
Unrud
5771f44b0e Cleanup portal controller 2025-03-18 09:18:16 +01:00
Unrud
16dc47b570 Encrypt restore token 2025-03-18 09:18:15 +01:00
Unrud
abd9fd4773 Update dependencies 2025-03-17 20:48:45 +01:00
Unrud
7c1652e9dd CI: Install missing package 2025-03-17 20:24:25 +01:00
godalming
0fe41d0b15 Fix not being able to create restore token on snap because the cache directory does not exist 2025-03-17 20:21:35 +01:00
godalming
21461d9a76 Fix file permisions. Detect if restore tokens are not supported (version <= 1). Always log and continue if it is not possible to use a restore token rather than exit the program or log using fmt. 2025-03-17 00:19:48 +01:00
godalming
c6c54c77a7 Add support for session restore on the linux portals backend, fixing #77 2025-03-17 00:19:48 +01:00
Unrud
7a806f7b00 Flatpak: Update runtime 2024-11-17 21:33:51 +01:00
Unrud
4fb452c542 Bump version to 1.4.8 2024-09-01 17:15:32 +02:00
Unrud
c71e5c4b8b Flatpak: Add IPC to fix linting error 2024-09-01 17:12:02 +02:00
Unrud
30cfff86b0 Update go.sum 2024-09-01 17:09:23 +02:00
Unrud
288345b5d3 Bump version to 1.4.7 2024-08-30 23:45:18 +02:00
Unrud
b4ecfad316 Update dependencies 2024-08-30 23:41:45 +02:00
Unrud
dd510bc4a0 Update appdata 2024-04-13 19:03:36 +02:00
Unrud
4916fc59db Update dependencies 2024-04-06 17:22:46 +02:00
Unrud
064c3cd242 appdata: Add vcs-browser URL 2024-04-05 22:38:15 +02:00
Unrud
6996fbf958 appdata: Add developer ID 2024-04-05 22:37:48 +02:00
Unrud
016351e9d2 Bump version to 1.4.6 2024-02-06 11:33:41 +01:00
Unrud
6325782058 Fix CI test 2024-02-06 11:33:40 +01:00
Unrud
0184dcb22f Set MIME types explicitly
Fixes #61
Fixes #70
2024-02-06 11:26:34 +01:00
Unrud
652ee5aa6c Bump version to 1.4.5 2024-01-31 14:59:38 +01:00
Unrud
88fd6e7314 Workaround for virtual keyboard covering buttons for Safari on iOS 2024-01-31 09:33:11 +01:00
Unrud
6a812607fe Fix virtual keyboard covering buttons in Chromium 2024-01-31 09:33:11 +01:00
Unrud
f11cefc74c Keep text input open after sending 2024-01-31 09:33:10 +01:00
Unrud
7cff8286c8 Hide focus outline for textarea 2024-01-31 09:33:10 +01:00
Unrud
280a654f59 Enable hardware keyboard input in mouse mode 2024-01-31 09:33:09 +01:00
Unrud
d502ca651c Disable keyboard navigation for all buttons 2024-01-31 09:33:09 +01:00
Unrud
1985842ff0 Refactor CSS 2024-01-31 09:33:09 +01:00
Unrud
fc363fe404 Snap: Update to core22 2024-01-27 07:40:22 +01:00
Unrud
cc143b59e6 Bump version to 1.4.4 2024-01-23 20:51:57 +01:00
Unrud
02d49bf680 Flatpak: Update dependencies 2024-01-23 20:51:53 +01:00
Unrud
601d8c8358 Update dependencies 2024-01-23 19:42:59 +01:00
Unrud
0ca1431594 Fix Xwayland detection with newer servers
Fixes #63
2024-01-23 19:33:15 +01:00
Unrud
48ff3d67a0 Fix minimum go version
Fixes #62
2024-01-23 14:14:29 +01:00
Unrud
e94fb986c0 Fix typo that causes error with xdg-desktop-portal-kde
Fixes #67
2024-01-23 14:11:16 +01:00
Unrud
7f7604a462 Bump version to 1.4.3 2023-10-04 01:44:59 +02:00
Unrud
e1dbdc9b68 Update dependencies 2023-10-04 01:41:25 +02:00
Unrud
6c746b23f1 Bump version to 1.4.2 2023-05-21 01:01:22 +02:00
Unrud
c8e6664a8d Fix Windows controller
Partially revert 1794150285
Fixes #58
2023-05-21 00:53:33 +02:00
Unrud
1803577892 Bump version to 1.4.1 2023-05-11 23:03:46 +02:00
Unrud
4358c0b0dc Update dependencies 2023-05-11 22:55:04 +02:00
Unrud
737c9671cf Flatpak: Update dependencies 2023-05-09 00:10:03 +02:00
Unrud
925cb745eb Run tests with github actions 2023-05-08 23:58:02 +02:00
Unrud
722678ca4f Update dependencies 2023-05-08 22:49:45 +02:00
Unrud
c6fe56479a uinput: Fix vertical scroll direction 2023-05-08 22:31:34 +02:00
Unrud
1794150285 Simplify error handling 2023-05-08 22:31:02 +02:00
Unrud
6a1a7420dc Update dependencies 2023-05-08 17:48:24 +02:00
Unrud
13f5df3bf8 Bump verstion to 1.4.0 2023-04-25 16:40:10 +02:00
Unrud
bb25f2dd37 Flatpak: Update vendor packages 2023-04-25 16:32:01 +02:00
Unrud
af116f4cfb Explain prevention of mouse mode by touch 2023-04-25 16:31:41 +02:00
Unrud
3c2bec8916 Update dependencies 2023-04-25 01:41:43 +02:00
Unrud
45801d7cd5 Add numeric keypad
Fixes #54
2023-04-25 01:38:52 +02:00
Unrud
4285f6aa1b Add visual and vibration feedback to buttons 2023-04-25 01:38:51 +02:00
Unrud
20cea84056 Prevent touch triggering mouse mode 2023-04-25 01:38:50 +02:00
Unrud
b239ca7160 Use ECMAScript modules 2023-04-25 01:38:50 +02:00
Unrud
e5369af219 Refactor web 2023-04-25 01:38:49 +02:00
Unrud
51108765e3 Add null controller 2023-04-25 01:38:49 +02:00
Unrud
37dff3cb6b uinput: Detect unsupported platform 2023-04-25 01:38:48 +02:00
Unrud
891fde7051 Refactor errors 2023-04-25 01:38:48 +02:00
Unrud
91a37de0fb Bump version to 1.3.0 2023-03-03 23:14:34 +01:00
Unrud
d81b57b1f9 Add uinput build example 2023-03-03 23:06:34 +01:00
Unrud
0bd7260a0c uinput-controller: Show hint for setting keymap 2023-02-27 23:43:10 +01:00
Unrud
e69bd15733 uinput-controller: Lower priority 2023-02-27 23:42:16 +01:00
Unrud
bbb6cc4dc1 uinput-controller: Initial support for keymaps
Keymap can be set with environment variable REMOTE_TOUCHPAD_UINPUT_KEYMAP
2023-02-26 16:45:37 +01:00
Unrud
d254ae5b0d uinput-controller: Map runes/keys directly and handle errors 2023-02-23 02:07:14 +01:00
Unrud
809d3a0064 uinput-controller: Format code 2023-02-23 01:09:29 +01:00
De_Coder
feaf02217c improve uinput controller 2023-02-23 01:09:11 +01:00
De_Coder
f5d19a0354 Add uinput-based controller 2023-02-23 01:09:11 +01:00
Unrud
342334ac18 Update dependencies 2023-02-23 01:09:05 +01:00
Unrud
124152f7ec Include missing rune, key or button in error 2023-02-23 01:09:04 +01:00
Unrud
6f03281fc7 Refactor: Use switch statement 2023-02-23 01:09:03 +01:00
Unrud
79a88dc676 Bump version to 1.2.3 2022-12-29 22:32:47 +01:00
Unrud
9c5dcbee10 Update dependencies 2022-12-29 22:32:47 +01:00
Unrud
f30b4cf48e Flatpak: Add Wayland socket to disable X11 socket 2022-12-29 22:32:38 +01:00
Unrud
681d9c4f1d Improve XWayland detection 2022-12-29 22:19:38 +01:00
Unrud
2fc5d0825b Flatpak: Remove x11 from finish-args 2022-11-03 01:56:15 +01:00
Unrud
c1491f4a84 Snap: Update to core20 2022-09-28 00:48:41 +02:00
Unrud
b96944d4d6 Bump version to 1.2.2 2022-09-24 02:15:04 +02:00
Unrud
61f910adf0 Update dependencies 2022-09-24 02:09:15 +02:00
Unrud
518c6ef952 Fix race in RemoteDesktop portal controller 2022-09-24 02:09:05 +02:00
Unrud
1b3d26c465 Flatpak: Update runtime 2022-09-04 22:13:57 +02:00
Unrud
1675d5d2f8 Bump version to 1.2.1 2022-06-20 23:11:22 +02:00
Unrud
8a2a29c9d6 Reorganize 2022-06-20 03:48:43 +02:00
Unrud
e29982dc00 Update dependencies 2022-06-20 01:19:01 +02:00
Unrud
7522d131ec Update to go 1.17 2022-06-20 01:19:01 +02:00
Unrud
6134fa564b Flatpak: Generate vendor/modules.txt 2022-06-20 01:19:00 +02:00
Unrud
523d2039b6 Bump version to 1.2.0 2022-02-26 13:53:17 +01:00
Unrud
771eea98b5 Increase update rate 2022-02-26 13:53:17 +01:00
Unrud
d6b75771c3 Throw exception again 2022-02-23 21:11:24 +01:00
Unrud
90922d85c6 Return early if no relevant touches 2022-02-23 21:11:23 +01:00
Unrud
0c401e0247 Add missing semicolons and initialize variables 2022-02-22 23:36:56 +01:00
Unrud
b748aceaf9 Prevent soft lockup if an error occurs 2022-02-22 23:36:56 +01:00
Unrud
a9117b82ad Update changelog 2022-02-22 22:40:30 +01:00
Unrud
9543c514d0 Show error message if compiled without controller 2022-02-22 22:40:07 +01:00
Unrud
238ef14843 Rename variable 2022-02-22 22:19:37 +01:00
Unrud
190b2763bd Organize code in namespaces 2022-02-22 22:05:48 +01:00
Unrud
9432869bce Limit update rate 2022-02-22 20:27:39 +01:00
Unrud
61fa13974d README: Switch from "go get" to "go install" 2022-02-22 16:23:00 +01:00
Unrud
e027c71341 Normalize line endings client side 2022-02-22 16:17:55 +01:00
Unrud
a279e195be Windows: Reverse vertical scroll direction 2022-02-21 22:30:29 +01:00
Unrud
25d5cfa505 Rename backend to controller 2022-02-21 22:30:28 +01:00
Unrud
2211b8a662 Remove disabled backends 2022-02-21 22:30:28 +01:00
Unrud
89131db79b Centralized UI updates 2022-02-21 22:30:27 +01:00
Unrud
42dcafda3d Improve variable names 2022-02-21 22:30:26 +01:00
Unrud
9082ca434a Add fullscreenchange event listener 2022-02-21 22:30:26 +01:00
Unrud
98f4d1c692 Add options for configuring pointer and scroll speed 2022-02-21 22:30:25 +01:00
Unrud
fecec13869 Add hardware mouse support 2022-02-21 22:30:20 +01:00
Unrud
1797d7585b Prevent clicks from touches on touchpad 2022-02-21 22:30:20 +01:00
Unrud
db9ca255fa Add Touch prefix to handlers 2022-02-21 22:30:19 +01:00
Unrud
b3db523db1 Keep move and scroll remainder 2022-02-21 22:30:19 +01:00
Unrud
51dbecaa63 Split function updateMoveAndScroll 2022-02-21 22:30:19 +01:00
Unrud
ab09e7c674 Refactor key detection 2022-02-21 22:30:18 +01:00
Unrud
56cff8deb2 Listen for keydown event on document 2022-02-21 22:30:18 +01:00
Unrud
ad3847f5aa Combine scroll and scroll finish commands 2022-02-21 22:30:18 +01:00
Unrud
1f3aa58568 Remove padlabel id 2022-02-21 22:30:17 +01:00
Unrud
41c3e0b10d Switch to go:embed 2022-02-21 22:30:16 +01:00
Unrud
7fc46bf38c Rename class touchpad to touch 2022-02-03 14:51:54 +01:00
Unrud
b3a4ca7449 Use go-bindata instead of go-bindata-assetfs 2022-02-03 14:45:35 +01:00
Unrud
abc8ef5818 Workaround for assetFS FileInfo
See https://github.com/elazarl/go-bindata-assetfs/pull/55
2022-02-01 13:59:32 +01:00
Unrud
c0e6ef52ec Use camel case for variable name 2022-02-01 13:59:32 +01:00
Unrud
15b05b5fa6 Bump version to 1.1.0 2022-01-29 11:04:23 +01:00
Unrud
a4cac5fb54 Update screenshots 2022-01-29 11:04:22 +01:00
Unrud
7392eec8f1 Update dependencies 2022-01-29 11:04:22 +01:00
Unrud
3feddd5f68 Ignore unparseable IP 2022-01-29 11:04:21 +01:00
Unrud
42528c0cf7 Use class for scenes and fix resizing keys 2022-01-29 11:04:21 +01:00
Unrud
c85275f263 Add back button to keys scene and allow fullscreen
Closes #29
2022-01-28 00:14:20 +01:00
Unrud
facaacb3fb Use history instead of window.history 2022-01-28 00:13:19 +01:00
Unrud
077dc6def0 Check modifier keys for physical keyboard 2022-01-28 00:12:55 +01:00
Unrud
e702b239eb Enable touchpad and keyboard on keys page
Fixes #25
2022-01-27 17:00:50 +01:00
Unrud
cf8753698f Save keys page in session 2022-01-27 16:55:50 +01:00
Unrud
632c944561 Use classes for fullscreen and pad capability 2022-01-27 16:55:50 +01:00
Unrud
c749865ef1 Save keyboard text in session 2022-01-27 16:55:50 +01:00
Unrud
24f3eb63ec Cosmetics
Rename variable keys_pages to keysPages.
Rename variable page_index to pageIndex.
Rename textarea text to keyboardtext.
Rename button textkeysbutton to keyboardkeysbutton.
Omit last , in array.
2022-01-27 16:51:33 +01:00
Unrud
a113b3adb7 Add support for physical keyboard 2022-01-27 07:26:31 +01:00
Unrud
a23a1260e9 Cosmetics 2022-01-27 07:17:35 +01:00
Unrud
5da7b376c9 Add return key 2022-01-27 07:17:23 +01:00
Unrud
957e116305 Invert QR code 2022-01-24 09:00:16 +01:00
Unrud
86cbc977b0 Print URL and QR code to stdout
Closes #30
2022-01-24 08:59:37 +01:00
Unrud
f741196640 Remove newline from QR code 2022-01-24 08:58:41 +01:00
Unrud
9806aa26e1 Cosmetics 2022-01-24 08:58:40 +01:00
Unrud
d22dc0f4dd Add more keys
Fixes #36
Fixes #27
Fixes #22
2022-01-24 08:08:21 +01:00
Unrud
92a46e6637 Improve icon font
Round values to integer
Change EM square to 1024
Move glyph uni2630 to uni2261
2022-01-24 07:52:34 +01:00
Unrud
cfad7e9d68 Use pointer as cursor for buttons 2022-01-24 07:52:27 +01:00
Unrud
348ec1f6ab Remove unused function 2022-01-24 07:52:19 +01:00
Unrud
64327f326c Cosmetics 2022-01-24 07:52:09 +01:00
Unrud
dd2d13b4e4 Compatibility with ECMA Version 5
Fixes #40
2022-01-17 15:18:56 +01:00
Unrud
02fa08e266 Bump version to 1.0.5 2022-01-08 00:37:46 +01:00
Unrud
70e63a57af Update dependencies 2022-01-08 00:25:02 +01:00
Unrud
130c430d3d Use relative URL for websocket
Partially merged from branch 'schnusch-http-prefix'
2022-01-08 00:23:59 +01:00
schnusch
47e9bb2047 use relative URL for websocket 2022-01-04 14:51:18 +01:00
Unrud
55b0ba894e Bump version to 1.0.4 2021-11-06 21:45:02 +01:00
Unrud
e2ee520380 Disable touch-action (closes #37) 2021-11-06 21:45:01 +01:00
Unrud
c53252c197 Update dependencies 2021-11-06 21:45:01 +01:00
79 changed files with 5514 additions and 2373 deletions

43
.github/workflows/test.yaml vendored Normal file
View file

@ -0,0 +1,43 @@
name: Test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
os: [linux]
tags: [[], ['null'], [portal], [uinput], [x11],
['null', portal, uinput, x11]]
include:
- os: windows
tags: []
- os: windows
tags: ['null']
env:
GOOS: ${{ matrix.os }}
GOFLAGS: -tags=${{ join(matrix.tags, ',') }}
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
with:
go-version: stable
check-latest: true
- run: sudo apt install libxrandr-dev libxtst-dev libxt-dev
if: ${{ contains(matrix.tags, 'x11') }}
- name: Go Format
run: gofmt -s -w . && git diff --exit-code
- name: Go Mod Tidy
run: go mod tidy && git diff --exit-code
- name: Go Vet
run: go vet ./...
- name: Go Build
run: go build ./...
- name: Go Test (with data race detection)
run: go test -race ./...
if: ${{ matrix.os == 'linux' }}

11
.gitignore vendored Normal file
View file

@ -0,0 +1,11 @@
# Binaries
remote-touchpad
# Systemd service state
.remote-touchpad.service.state
# Logs and run files
.rts-log
# Environment files
.env

View file

@ -1,5 +1,119 @@
# Changelog
## 1.5.4 (2026-08-02)
* Improve X11 keyboard input
* Update dependencies
## 1.5.3 (2025-12-24)
* Update dependencies
## 1.5.2 (2025-10-12)
* Update dependencies
## 1.5.0 (2025-03-18)
* Add restore token support for the remote desktop portal
* Update dependencies
## 1.4.8 (2024-09-01)
* Update dependencies
## 1.4.7 (2024-08-30)
* Update dependencies
## 1.4.6 (2024-02-06)
* Prevent incorrect MIME types
## 1.4.5 (2024-01-31)
* Keep text input open after sending
* Workaround for virtual keyboard covering buttons (Chromium & iOS)
* Enable hardware keyboard input in mouse mode
## 1.4.4 (2024-01-23)
* Fix Xwayland detection on newer servers
* Fix support for the KDE remote desktop portal
* Set correct minimum version for Go
* Update dependencies
## 1.4.3 (2023-10-03)
* Update dependencies
## 1.4.2 (2023-05-20)
* Fix Windows support
## 1.4.1 (2023-05-11)
* uinput: Fix vertical scroll direction
* Update dependencies
## 1.4.0 (2023-04-25)
* Add feedback to buttons
* Add numeric keypad
* Prevent touch triggering mouse mode
## 1.3.0 (2023-03-03)
* Initial support for uinput
* Update dependencies
## 1.2.3 (2022-12-29)
* Improve XWayland detection
* Update dependencies
* Flatpak: Add Wayland socket to disable X11 socket
## 1.2.2 (2022-09-23)
* Update dependencies
* Fix race in RemoteDesktop portal controller
## 1.2.1 (2022-06-20)
* Update dependencies
## 1.2.0 (2022-02-26)
* Add command-line options for move and scroll speed
* Add hardware mouse support
* Limit update rate
* Windows: Reverse vertical scroll direction
* Show error message if compiled without controller
* Don't show disabled controllers in "unsupported platform" error message
* Switch from go-bindata-assetfs to go:embed
## 1.1.0 (2022-01-28)
* Add more shortcut keys (Arrows, Delete, Enter, …)
* Add basic support for physical keyboard
* Allow fullscreen on keys page
* Enable touchpad on keys page
* Retain keyboard text when reloading website
* Improve compatibility with old browsers
* Write URL and QR code to stdout
* Ignore unparseable IP
* Remove trailing new line from QR code
## 1.0.5 (2022-01-07)
* Use relative URL for websocket
* Update dependencies
## 1.0.4 (2021-11-06)
* Disable browser touch gestures
* Update dependencies
## 1.0.3 (2021-09-18)
* Update dependencies

View file

@ -12,8 +12,45 @@ Supports Flatpak's RemoteDesktop portal (for Wayland), Windows and X11.
* [Snap](https://snapcraft.io/remote-touchpad)
* [Windows](https://github.com/Unrud/remote-touchpad/releases/latest)
* Golang:
* Portal & X11: `go get -u -tags portal,x11 github.com/Unrud/remote-touchpad`
* Windows: `get get -u github.com/Unrud/remote-touchpad`
* Portal & uinput & X11:
```sh
go install -tags portal,uinput,x11 github.com/unrud/remote-touchpad@latest
```
* Windows:
```sh
go install github.com/unrud/remote-touchpad@latest
```
## Linux User Service Installation
To install remote-touchpad as a user-specific systemd service (no root access required):
1. Clone the repository and navigate to the project directory:
```sh
git clone https://github.com/Unrud/remote-touchpad.git
cd remote-touchpad
```
2. Install the service using the provided wrapper script (no sudo required):
```sh
./rts install
```
3. Access the service:
* View service status: `./rts status`
* Get URL: `./rts url`
* View logs: `./rts log`
* Stop service: `./rts stop`
* Start service: `./rts start`
* Restart service: `./rts restart`
* Uninstall: `./rts uninstall`
The service will run in the background and maintain a persistent connection with URL and QR code accessible via the `url` command.
## Screenshots

View file

@ -1,292 +0,0 @@
// +build portal
/*
* Copyright (c) 2018 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 (
"errors"
"fmt"
"github.com/godbus/dbus/v5"
"sync"
)
const (
deviceKeyboard uint32 = 1
devicePointer uint32 = 2
btnReleased uint32 = 0
btnPressed uint32 = 1
// linux/input-event-codes.h
btnLeft int32 = 0x110
btnRight int32 = 0x111
btnMiddle int32 = 0x112
)
type portalBackend struct {
bus *dbus.Conn
remoteDesktop dbus.BusObject
sessionHandle dbus.ObjectPath
lock sync.RWMutex
}
func InitPortalBackend() (Backend, error) {
bus, err := dbus.SessionBusPrivate()
if err != nil {
return nil, UnsupportedPlatformError{err}
}
cleanupBus := true
defer func() {
if cleanupBus {
bus.Close()
}
}()
err = bus.Auth(nil)
if err != nil {
return nil, UnsupportedPlatformError{err}
}
err = bus.Hello()
if err != nil {
return nil, UnsupportedPlatformError{err}
}
remoteDesktop := bus.Object("org.freedesktop.portal.Desktop",
"/org/freedesktop/portal/desktop")
availableDeviceTypesV, err := remoteDesktop.GetProperty(
"org.freedesktop.portal.RemoteDesktop.AvailableDeviceTypes")
if err != nil {
return nil, UnsupportedPlatformError{err}
}
availableDeviceTypes, ok := availableDeviceTypesV.Value().(uint32)
if !ok {
return nil, UnsupportedPlatformError{errors.New(
"unexpected 'AvailableDeviceTypes' return type")}
}
if availableDeviceTypes&deviceKeyboard == 0 ||
availableDeviceTypes&devicePointer == 0 {
return nil, UnsupportedPlatformError{errors.New(
"keyboard or pointer source type not supported")}
}
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{err}
}
if result != 0 {
return nil, UnsupportedPlatformError{errors.New(fmt.Sprintf(
"Calling 'CreateSession' failed (%v)", result))}
}
sessionHandleV, ok := outVardict["session_handle"]
if !ok {
return nil, UnsupportedPlatformError{errors.New(
"'session_handle' missing from 'CreateSession' return value")}
}
sessionHandleS, ok := sessionHandleV.Value().(string)
if !ok {
return nil, UnsupportedPlatformError{errors.New(
"unexpected 'session_handle' type in 'CreateSession' return value")}
}
sessionHandle := dbus.ObjectPath(sessionHandleS)
inVardict = make(map[string]dbus.Variant)
inVardict["type"] = dbus.MakeVariant(deviceKeyboard | devicePointer)
result, outVardict, err = getResponse(bus, remoteDesktop,
"org.freedesktop.portal.RemoteDesktop.SelectDevices", 0, sessionHandle, inVardict)
if err != nil {
return nil, UnsupportedPlatformError{err}
}
if result != 0 {
return nil, UnsupportedPlatformError{errors.New(fmt.Sprintf(
"Calling 'SelectDevices' failed (%v)", result))}
}
inVardict = make(map[string]dbus.Variant)
result, outVardict, err = getResponse(bus, remoteDesktop,
"org.freedesktop.portal.RemoteDesktop.Start", 0, sessionHandle, "", inVardict)
if err != nil {
return nil, UnsupportedPlatformError{err}
}
if result != 0 {
return nil, errors.New("keyboard or pointer access denied")
}
devicesV, ok := outVardict["devices"]
if !ok {
return nil, UnsupportedPlatformError{errors.New(
"'devices' missing from 'Start' return value")}
}
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 &portalBackend{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) {
ch := make(chan *dbus.Signal)
bus.Signal(ch)
defer bus.RemoveSignal(ch)
var requestPath dbus.ObjectPath
if err := object.Call(method, flags, args...).Store(&requestPath); err != nil {
return 0, nil, err
}
for {
s := <-ch
if s.Path == requestPath && s.Name == "org.freedesktop.portal.Request.Response" {
if len(s.Body) != 2 {
return 0, nil, errors.New("unexpected 'Response' return length")
}
result, ok := s.Body[0].(uint32)
if !ok {
return 0, nil, errors.New("unexpected 'Response' return type")
}
outVardict, ok := s.Body[1].(map[string]dbus.Variant)
if !ok {
return 0, nil, errors.New("unexpected 'Response' return type")
}
return result, outVardict, nil
}
}
}
func (p *portalBackend) Close() error {
p.lock.Lock()
defer p.lock.Unlock()
if p.bus == nil {
return errors.New("dbus connection closed")
}
if err := p.bus.Close(); err != nil {
return err
}
p.bus = nil
p.remoteDesktop = nil
return nil
}
func (p *portalBackend) keyboardKeys(keys []Keysym) error {
p.lock.RLock()
defer p.lock.RUnlock()
if p.bus == nil {
return errors.New("dbus connection closed")
}
inVardict := make(map[string]dbus.Variant)
for _, keysym := range keys {
for _, state := range [...]uint32{btnPressed, btnReleased} {
if err := p.remoteDesktop.Call(
"org.freedesktop.portal.RemoteDesktop.NotifyKeyboardKeysym",
0, p.sessionHandle, inVardict, keysym, state).Store(); err != nil {
return err
}
}
}
return nil
}
func (p *portalBackend) KeyboardText(text string) error {
keys := make([]Keysym, 0, len(text))
for _, runeValue := range text {
keysym, err := RuneToKeysym(runeValue)
if err != nil {
return err
}
keys = append(keys, keysym)
}
return p.keyboardKeys(keys)
}
func (p *portalBackend) KeyboardKey(key Key) error {
keysym, err := KeyToKeysym(key)
if err != nil {
return err
}
keys := [...]Keysym{keysym}
return p.keyboardKeys(keys[:])
}
func (p *portalBackend) PointerButton(button PointerButton, press bool) error {
p.lock.RLock()
defer p.lock.RUnlock()
if p.bus == nil {
return errors.New("dbus connection closed")
}
var btn int32
if button == PointerButtonLeft {
btn = btnLeft
} else if button == PointerButtonMiddle {
btn = btnMiddle
} else if button == PointerButtonRight {
btn = btnRight
} else {
return errors.New("unsupported pointer button")
}
state := btnReleased
if press {
state = btnPressed
}
inVardict := make(map[string]dbus.Variant)
if err := p.remoteDesktop.Call("org.freedesktop.portal.RemoteDesktop.NotifyPointerButton",
0, p.sessionHandle, inVardict, btn, state).Store(); err != nil {
return err
}
return nil
}
func (p *portalBackend) PointerMove(deltaX, deltaY int) error {
p.lock.RLock()
defer p.lock.RUnlock()
if p.bus == nil {
return errors.New("dbus connection closed")
}
inVardict := make(map[string]dbus.Variant)
if err := p.remoteDesktop.Call("org.freedesktop.portal.RemoteDesktop.NotifyPointerMotion",
0, p.sessionHandle, inVardict, float64(deltaX), float64(deltaY)).Store(); err != nil {
return err
}
return nil
}
func (p *portalBackend) pointerScrollFull(deltaHorizontal, deltaVertical int, finish bool) error {
p.lock.RLock()
defer p.lock.RUnlock()
if p.bus == nil {
return errors.New("dbus connection closed")
}
inVardict := make(map[string]dbus.Variant)
inVardict["finish"] = dbus.MakeVariant(finish)
if err := p.remoteDesktop.Call("org.freedesktop.portal.RemoteDesktop.NotifyPointerAxis",
0, p.sessionHandle, inVardict, float64(deltaHorizontal), float64(deltaVertical)).Store(); err != nil {
return err
}
return nil
}
func (p *portalBackend) PointerScroll(deltaHorizontal, deltaVertical int) error {
return p.pointerScrollFull(deltaHorizontal, deltaVertical, false)
}
func (p *portalBackend) PointerScrollFinish() error {
return p.pointerScrollFull(0, 0, true)
}

View file

@ -1,28 +0,0 @@
// +build !x11
/*
* Copyright (c) 2018 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 "errors"
func InitX11Backend() (Backend, error) {
return nil, UnsupportedPlatformError{errors.New("disabled")}
}

File diff suppressed because one or more lines are too long

View file

@ -1,4 +0,0 @@
package main
//go:generate go-bindata-assetfs -mode 0664 -modtime 1 -o ./bindata.generated.go webdata/...
//go:generate gofmt -w ./bindata.generated.go

View file

@ -17,8 +17,190 @@
<launchable type="desktop-id">com.github.unrud.RemoteTouchpad.desktop</launchable>
<url type="bugtracker">https://github.com/Unrud/remote-touchpad/issues</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>
<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">
<description>
<ul>
<li>Keep text input open after sending</li>
<li>Workaround for virtual keyboard covering buttons (Chromium &amp; iOS)</li>
<li>Enable hardware keyboard input in mouse mode</li>
</ul>
</description>
</release>
<release version="1.4.4" date="2024-01-23">
<description>
<ul>
<li>Fix Xwayland detection on newer servers</li>
<li>Fix support for the KDE remote desktop portal</li>
<li>Set correct minimum version for Go</li>
<li>Update dependencies</li>
</ul>
</description>
</release>
<release version="1.4.3" date="2023-10-03">
<description>
<ul>
<li>Update dependencies</li>
</ul>
</description>
</release>
<release version="1.4.2" date="2023-05-20">
<description>
<ul>
<li>Fix Windows support</li>
</ul>
</description>
</release>
<release version="1.4.1" date="2023-05-11">
<description>
<ul>
<li>uinput: Fix vertical scroll direction</li>
<li>Update dependencies</li>
</ul>
</description>
</release>
<release version="1.4.0" date="2023-04-25">
<description>
<ul>
<li>Add feedback to buttons</li>
<li>Add numeric keypad</li>
<li>Prevent touch triggering mouse mode</li>
<li>Update dependencies</li>
</ul>
</description>
</release>
<release version="1.3.0" date="2023-03-03">
<description>
<ul>
<li>Initial support for uinput</li>
<li>Update dependencies</li>
</ul>
</description>
</release>
<release version="1.2.3" date="2022-12-29">
<description>
<ul>
<li>Improve XWayland detection</li>
<li>Update dependencies</li>
<li>Flatpak: Add Wayland socket to disable X11 socket</li>
</ul>
</description>
</release>
<release version="1.2.2" date="2022-09-23">
<description>
<ul>
<li>Update dependencies</li>
<li>Fix race in RemoteDesktop portal controller</li>
</ul>
</description>
</release>
<release version="1.2.1" date="2022-06-20">
<description>
<ul>
<li>Update dependencies</li>
</ul>
</description>
</release>
<release version="1.2.0" date="2022-02-26">
<description>
<ul>
<li>Add command-line options for move and scroll speed</li>
<li>Add hardware mouse support</li>
<li>Limit update rate</li>
<li>Windows: Reverse vertical scroll direction</li>
<li>Show error message if compiled without controller</li>
<li>Don't show disabled controllers in "unsupported platform" error message</li>
<li>Switch from go-bindata-assetfs to go:embed</li>
</ul>
</description>
</release>
<release version="1.1.0" date="2022-01-28">
<description>
<ul>
<li>Add more shortcut keys (Arrows, Delete, Enter, …)</li>
<li>Add basic support for physical keyboard</li>
<li>Allow fullscreen on keys page</li>
<li>Enable touchpad on keys page</li>
<li>Retain keyboard text when reloading website</li>
<li>Improve compatibility with old browsers</li>
<li>Write URL and QR code to stdout</li>
<li>Ignore unparseable IP</li>
<li>Remove trailing new line from QR code</li>
</ul>
</description>
</release>
<release version="1.0.5" date="2022-01-07">
<description>
<ul>
<li>Use relative URL for websocket</li>
<li>Update dependencies</li>
</ul>
</description>
</release>
<release version="1.0.4" date="2021-11-06">
<description>
<ul>
<li>Disable browser touch gestures</li>
<li>Update dependencies</li>
</ul>
</description>
</release>
<release version="1.0.3" date="2021-09-18">
<description>
<ul>
@ -191,16 +373,16 @@
</releases>
<screenshots>
<screenshot type="default">
<image>https://raw.githubusercontent.com/Unrud/remote-touchpad/v1.0.3/screenshots/1.png</image>
<image>https://raw.githubusercontent.com/Unrud/remote-touchpad/v1.5.4/screenshots/1.png</image>
</screenshot>
<screenshot>
<image>https://raw.githubusercontent.com/Unrud/remote-touchpad/v1.0.3/screenshots/2.png</image>
<image>https://raw.githubusercontent.com/Unrud/remote-touchpad/v1.5.4/screenshots/2.png</image>
</screenshot>
<screenshot>
<image>https://raw.githubusercontent.com/Unrud/remote-touchpad/v1.0.3/screenshots/3.png</image>
<image>https://raw.githubusercontent.com/Unrud/remote-touchpad/v1.5.4/screenshots/3.png</image>
</screenshot>
<screenshot>
<image>https://raw.githubusercontent.com/Unrud/remote-touchpad/v1.0.3/screenshots/4.png</image>
<image>https://raw.githubusercontent.com/Unrud/remote-touchpad/v1.5.4/screenshots/4.png</image>
</screenshot>
</screenshots>
<content_rating type="oars-1.1"/>

View file

@ -1,27 +1,24 @@
app-id: com.github.unrud.RemoteTouchpad
runtime: org.freedesktop.Platform
runtime-version: '21.08'
runtime-version: '25.08'
sdk: org.freedesktop.Sdk
command: remote-touchpad
sdk-extensions:
- org.freedesktop.Sdk.Extension.golang
finish-args:
- --socket=x11
- --socket=fallback-x11
- --socket=wayland
- --share=ipc # Required by Flathub linter
- --share=network
modules:
- name: remote-touchpad
sources:
# Vendor dependencies: go mod vendor -v
- type: git
url: https://github.com/elazarl/go-bindata-assetfs
tag: v1.0.1
commit: 234c15e7648ff35458026de92b34c637bae5e6f7
dest: vendor/github.com/elazarl/go-bindata-assetfs
path: ..
- type: git
url: https://github.com/godbus/dbus
tag: v5.0.5
commit: 84a2dfb382a81e88c000206b8fcba8d98f00ad1e
tag: v5.2.2
commit: a8ac15ba63645f02ffd57f4b443203279ab40b30
dest: vendor/github.com/godbus/dbus/v5
- type: git
url: https://github.com/skip2/go-qrcode
@ -29,14 +26,17 @@ modules:
dest: vendor/github.com/skip2/go-qrcode
- type: git
url: https://github.com/golang/net
commit: 978cfadd31cf6299758dbb5165d451ce91989f1a
tag: v0.57.0
commit: b8f09f6f062ceb4531b7af4bd17a5c8fe9c4b2b5
dest: vendor/golang.org/x/net
- type: git
url: https://github.com/golang/sys
tag: v0.47.0
commit: 9e7e939dcafac07e8ab4cffa6e5fc74908413f00
dest: vendor/golang.org/x/sys
- type: file
path: modules.txt
dest: vendor
- type: git
path: ..
branch: master
buildsystem: simple
build-commands:
- . /usr/lib/sdk/golang/enable.sh; go build -tags portal,x11

View file

@ -1,14 +1,17 @@
# github.com/elazarl/go-bindata-assetfs v1.0.1
## explicit
github.com/elazarl/go-bindata-assetfs
# github.com/godbus/dbus/v5 v5.0.5
## explicit
# 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
## 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.0.0-20210917221730-978cfadd31cf
## explicit
# 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

10
go.mod
View file

@ -1,10 +1,12 @@
module github.com/unrud/remote-touchpad
go 1.15
go 1.25.5
require (
github.com/elazarl/go-bindata-assetfs v1.0.1
github.com/godbus/dbus/v5 v5.0.5
github.com/bendahl/uinput v1.7.0
github.com/godbus/dbus/v5 v5.2.2
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
golang.org/x/net v0.0.0-20210917221730-978cfadd31cf
golang.org/x/net v0.57.0
)
require golang.org/x/sys v0.47.0 // indirect

19
go.sum
View file

@ -1,13 +1,10 @@
github.com/elazarl/go-bindata-assetfs v1.0.1 h1:m0kkaHRKEu7tUIUFVwhGGGYClXvyl4RE03qmvRTNfbw=
github.com/elazarl/go-bindata-assetfs v1.0.1/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4=
github.com/godbus/dbus/v5 v5.0.5 h1:9Eg0XUhQxtkV8ykTMKtMMYY72g4NgxtRq4jgh4Ih5YM=
github.com/godbus/dbus/v5 v5.0.5/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/bendahl/uinput v1.7.0 h1:nA4fm8Wu8UYNOPykIZm66nkWEyvxzfmJ8YC02PM40jg=
github.com/bendahl/uinput v1.7.0/go.mod h1:Np7w3DINc9wB83p12fTAM3DPPhFnAKP0WTXRqCQJ6Z8=
github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
golang.org/x/net v0.0.0-20210917221730-978cfadd31cf h1:R150MpwJIv1MpS0N/pc+NhTM8ajzvlmxlY5OYsrevXQ=
golang.org/x/net v0.0.0-20210917221730-978cfadd31cf/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=

10
host.go
View file

@ -28,7 +28,7 @@ import (
const ipv4Rating int = +1
func FindDefaultHost() string {
func findDefaultHost() string {
type hostsValue struct {
prio int
host string
@ -53,10 +53,9 @@ func FindDefaultHost() string {
panic(err)
}
ip := net.ParseIP(host)
if ip == nil {
panic("Invalid IP address: " + host)
if ip != nil {
addIP(100, ip)
}
addIP(100, ip)
}
interfaces, _ := net.Interfaces()
for _, inter := range interfaces {
@ -75,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

View file

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Before After
Before After

View file

@ -0,0 +1,123 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20)"
sodipodi:docname="U+2191 Upwards Arrow.svg"
width="1000"
height="1000"
viewBox="0 0 264.58333 264.58334"
version="1.1"
id="svg964"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<sodipodi:namedview
inkscape:document-rotation="0"
inkscape:snap-global="false"
inkscape:current-layer="layer1"
inkscape:window-maximized="1"
inkscape:window-y="32"
inkscape:window-x="0"
inkscape:cy="397.51809"
inkscape:cx="371.25037"
inkscape:zoom="0.5710432"
inkscape:guide-bbox="true"
showguides="true"
showgrid="false"
id="namedview8"
inkscape:window-height="1011"
inkscape:window-width="1920"
inkscape:pageshadow="2"
inkscape:pageopacity="0"
guidetolerance="10"
gridtolerance="10"
objecttolerance="10"
borderopacity="1"
bordercolor="#666666"
pagecolor="#ffffff"
inkscape:pagecheckerboard="0">
<sodipodi:guide
inkscape:color="rgb(0,0,255)"
inkscape:label=""
inkscape:locked="false"
id="guide815"
orientation="0,1"
position="0,52.916667" />
<sodipodi:guide
inkscape:color="rgb(0,0,255)"
inkscape:label=""
inkscape:locked="false"
id="guide817"
orientation="1,0"
position="5.2916667,0" />
<sodipodi:guide
inkscape:color="rgb(0,0,255)"
inkscape:label=""
inkscape:locked="false"
id="guide819"
orientation="0,1"
position="0,259.29167" />
<sodipodi:guide
inkscape:color="rgb(0,0,255)"
inkscape:label=""
inkscape:locked="false"
id="guide821"
orientation="1,0"
position="259.29167,0" />
<sodipodi:guide
inkscape:color="rgb(0,0,255)"
inkscape:label=""
inkscape:locked="false"
id="guide823"
orientation="1,0"
position="29.104167,0" />
<sodipodi:guide
inkscape:color="rgb(0,0,255)"
inkscape:label=""
inkscape:locked="false"
id="guide825"
orientation="1,0"
position="235.47917,0" />
<sodipodi:guide
inkscape:color="rgb(0,0,255)"
inkscape:label=""
inkscape:locked="false"
id="guide827"
orientation="0,1"
position="132.29167,161.39583" />
<sodipodi:guide
inkscape:color="rgb(0,0,255)"
inkscape:label=""
inkscape:locked="false"
id="guide829"
orientation="1,0"
position="132.29167,161.39583" />
</sodipodi:namedview>
<defs
id="defs958" />
<metadata
id="metadata961">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-32.41665)">
<path
sodipodi:nodetypes="cccccccccccccccc"
d="m 220.86574,143.56816 c -0.0449,2.15968 -1.8086,3.88735 -3.96875,3.8871 h -59.46045 c -2.19179,2.2e-4 -3.96853,1.77696 -3.96875,3.96875 v 136.32811 c -2.2e-4,2.19179 -1.77696,3.96853 -3.96875,3.96875 h -34.43046 c -2.19179,-2.2e-4 -3.96854,-1.77696 -3.96875,-3.96875 V 151.42401 c -2.2e-4,-2.19179 -1.77696,-3.96853 -3.96875,-3.96875 H 47.670631 c -3.33572,-0.002 -5.18189,-3.86802 -3.08612,-6.46317 L 129.19795,39.176603 c 1.58863,-1.96551 4.58516,-1.96551 6.17379,0 l 84.61344,101.815487 c 0.58816,0.72802 0.89985,1.64034 0.88056,2.57607 z"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:7.9375;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
id="path841" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.1 KiB

View file

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Before After
Before After

View file

@ -0,0 +1,123 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20)"
sodipodi:docname="U+2193 Downwards Arrow.svg"
width="1000"
height="1000"
viewBox="0 0 264.58333 264.58334"
version="1.1"
id="svg964"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<sodipodi:namedview
inkscape:document-rotation="0"
inkscape:snap-global="false"
inkscape:current-layer="layer1"
inkscape:window-maximized="1"
inkscape:window-y="32"
inkscape:window-x="0"
inkscape:cy="397.51809"
inkscape:cx="371.25037"
inkscape:zoom="0.5710432"
inkscape:guide-bbox="true"
showguides="true"
showgrid="false"
id="namedview8"
inkscape:window-height="1011"
inkscape:window-width="1920"
inkscape:pageshadow="2"
inkscape:pageopacity="0"
guidetolerance="10"
gridtolerance="10"
objecttolerance="10"
borderopacity="1"
bordercolor="#666666"
pagecolor="#ffffff"
inkscape:pagecheckerboard="0">
<sodipodi:guide
inkscape:color="rgb(0,0,255)"
inkscape:label=""
inkscape:locked="false"
id="guide815"
orientation="0,1"
position="0,52.916667" />
<sodipodi:guide
inkscape:color="rgb(0,0,255)"
inkscape:label=""
inkscape:locked="false"
id="guide817"
orientation="1,0"
position="5.2916667,0" />
<sodipodi:guide
inkscape:color="rgb(0,0,255)"
inkscape:label=""
inkscape:locked="false"
id="guide819"
orientation="0,1"
position="0,259.29167" />
<sodipodi:guide
inkscape:color="rgb(0,0,255)"
inkscape:label=""
inkscape:locked="false"
id="guide821"
orientation="1,0"
position="259.29167,0" />
<sodipodi:guide
inkscape:color="rgb(0,0,255)"
inkscape:label=""
inkscape:locked="false"
id="guide823"
orientation="1,0"
position="29.104167,0" />
<sodipodi:guide
inkscape:color="rgb(0,0,255)"
inkscape:label=""
inkscape:locked="false"
id="guide825"
orientation="1,0"
position="235.47917,0" />
<sodipodi:guide
inkscape:color="rgb(0,0,255)"
inkscape:label=""
inkscape:locked="false"
id="guide827"
orientation="0,1"
position="132.29167,161.39583" />
<sodipodi:guide
inkscape:color="rgb(0,0,255)"
inkscape:label=""
inkscape:locked="false"
id="guide829"
orientation="1,0"
position="132.29167,161.39583" />
</sodipodi:namedview>
<defs
id="defs958" />
<metadata
id="metadata961">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-32.41665)">
<path
sodipodi:nodetypes="cccccccccccccccc"
d="m 43.696719,185.85518 c 0.04486,-2.15968 1.808597,-3.88735 3.96875,-3.8871 h 59.460451 c 2.19179,-2.2e-4 3.96853,-1.77696 3.96875,-3.96875 V 41.671221 c 2.2e-4,-2.19179 1.77696,-3.96853 3.96875,-3.96875 h 34.43046 c 2.19179,2.2e-4 3.96854,1.77696 3.96875,3.96875 V 177.99933 c 2.2e-4,2.19179 1.77696,3.96853 3.96875,3.96875 h 59.46045 c 3.33572,0.002 5.18189,3.86802 3.08612,6.46317 l -84.61344,101.81549 c -1.58863,1.96551 -4.58516,1.96551 -6.17379,0 L 44.577284,188.43125 c -0.588163,-0.72802 -0.899858,-1.64034 -0.880565,-2.57607 z"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:7.9375;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
id="path841" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.1 KiB

View file

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Before After
Before After

View file

@ -1,19 +1,19 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
id="svg964"
version="1.1"
viewBox="0 0 264.58333 264.58334"
height="1000"
width="1000"
sodipodi:docname="U+2630 Trigram for Heaven.svg"
inkscape:version="0.92.3 (2405546, 2018-03-11)">
sodipodi:docname="U+2261 Identical To .svg"
inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
@ -24,21 +24,22 @@
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1016"
inkscape:window-height="1011"
id="namedview8"
showgrid="false"
showguides="true"
inkscape:guide-bbox="true"
inkscape:zoom="1.1353809"
inkscape:cx="173.83003"
inkscape:cy="594.60287"
inkscape:zoom="0.56769045"
inkscape:cx="861.38493"
inkscape:cy="500.27264"
inkscape:window-x="0"
inkscape:window-y="27"
inkscape:window-y="32"
inkscape:window-maximized="1"
inkscape:current-layer="svg964"
inkscape:snap-global="true"
inkscape:measure-start="406.912,419.243"
inkscape:measure-end="406.912,580.422">
inkscape:measure-end="406.912,580.422"
inkscape:pagecheckerboard="0">
<sodipodi:guide
position="0,52.916667"
orientation="0,1"
@ -123,13 +124,13 @@
<path
inkscape:connector-curvature="0"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:7.9375;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 220.87358,203.33079 c -2.2e-4,-2.19179 -1.77696,-3.96852 -3.96875,-3.96875 H 47.678502 c -2.191787,2.3e-4 -3.968527,1.77696 -3.968747,3.96875 v 13.26379 c 2.2e-4,2.19179 1.77696,3.96854 3.968747,3.96875 H 216.90483 c 2.19179,-2.1e-4 3.96853,-1.77696 3.96875,-3.96875 z"
id="path825"
d="m 220.87358,182.16411 c -2.2e-4,-2.19179 -1.77696,-3.96852 -3.96875,-3.96875 H 47.678502 c -2.191787,2.3e-4 -3.968527,1.77696 -3.968747,3.96875 v 13.26379 c 2.2e-4,2.19179 1.77696,3.96854 3.968747,3.96875 H 216.90483 c 2.19179,-2.1e-4 3.96853,-1.77696 3.96875,-3.96875 z"
id="path200"
sodipodi:nodetypes="ccccccccc" />
<path
inkscape:connector-curvature="0"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:7.9375;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 220.87358,47.988759 c -2.2e-4,-2.19179 -1.77696,-3.96852 -3.96875,-3.96875 H 47.678502 c -2.191787,2.3e-4 -3.968527,1.77696 -3.968747,3.96875 V 61.25254 c 2.2e-4,2.19179 1.77696,3.968537 3.968747,3.968747 H 216.90483 c 2.19179,-2.1e-4 3.96853,-1.776957 3.96875,-3.968747 z"
id="path828"
d="m 220.87358,69.155427 c -2.2e-4,-2.19179 -1.77696,-3.96852 -3.96875,-3.96875 H 47.678502 c -2.191787,2.3e-4 -3.968527,1.77696 -3.968747,3.96875 v 13.263781 c 2.2e-4,2.19179 1.77696,3.968537 3.968747,3.968747 H 216.90483 c 2.19179,-2.1e-4 3.96853,-1.776957 3.96875,-3.968747 z"
id="path202"
sodipodi:nodetypes="ccccccccc" />
</svg>

Before

Width:  |  Height:  |  Size: 8 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

Before After
Before After

View file

@ -0,0 +1,123 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20)"
sodipodi:docname="U+2326 Erase To the Right.svg"
width="1000"
height="1000"
viewBox="0 0 264.58333 264.58334"
version="1.1"
id="svg964"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<sodipodi:namedview
inkscape:document-rotation="0"
inkscape:snap-global="true"
inkscape:current-layer="layer1"
inkscape:window-maximized="1"
inkscape:window-y="32"
inkscape:window-x="0"
inkscape:cy="432.54171"
inkscape:cx="961.39837"
inkscape:zoom="0.5710432"
inkscape:guide-bbox="true"
showguides="true"
showgrid="false"
id="namedview8"
inkscape:window-height="1011"
inkscape:window-width="1920"
inkscape:pageshadow="2"
inkscape:pageopacity="0"
guidetolerance="10"
gridtolerance="10"
objecttolerance="10"
borderopacity="1"
bordercolor="#666666"
pagecolor="#ffffff"
inkscape:pagecheckerboard="0">
<sodipodi:guide
inkscape:color="rgb(0,0,255)"
inkscape:label=""
inkscape:locked="false"
id="guide815"
orientation="0,1"
position="0,52.916667" />
<sodipodi:guide
inkscape:color="rgb(0,0,255)"
inkscape:label=""
inkscape:locked="false"
id="guide817"
orientation="1,0"
position="5.2916667,0" />
<sodipodi:guide
inkscape:color="rgb(0,0,255)"
inkscape:label=""
inkscape:locked="false"
id="guide819"
orientation="0,1"
position="0,259.29167" />
<sodipodi:guide
inkscape:color="rgb(0,0,255)"
inkscape:label=""
inkscape:locked="false"
id="guide821"
orientation="1,0"
position="259.29167,0" />
<sodipodi:guide
inkscape:color="rgb(0,0,255)"
inkscape:label=""
inkscape:locked="false"
id="guide823"
orientation="1,0"
position="29.104167,0" />
<sodipodi:guide
inkscape:color="rgb(0,0,255)"
inkscape:label=""
inkscape:locked="false"
id="guide825"
orientation="1,0"
position="235.47917,0" />
<sodipodi:guide
inkscape:color="rgb(0,0,255)"
inkscape:label=""
inkscape:locked="false"
id="guide827"
orientation="0,1"
position="132.29167,161.39583" />
<sodipodi:guide
inkscape:color="rgb(0,0,255)"
inkscape:label=""
inkscape:locked="false"
id="guide829"
orientation="1,0"
position="132.29167,161.39583" />
</sodipodi:namedview>
<defs
id="defs958" />
<metadata
id="metadata961">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-32.41665)">
<path
id="path841"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:7.9375;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 153.42494,76.12716 c 0.93573,-0.01929 1.84805,0.292402 2.57607,0.880565 L 257.8165,161.62116 c 1.96551,1.58863 1.96551,4.58516 0,6.17379 l -101.81549,84.61344 c -0.81646,0.65934 -1.75837,0.92725 -2.66651,0.88263 H 9.2409757 c -2.1917898,-2.2e-4 -3.9685409,-1.77696 -3.96875,-3.96875 V 181.92432 147.49386 80.09591 c 2.091e-4,-2.19179 1.7769602,-3.96853 3.96875,-3.96875 z M 45.447551,116.19655 c -1.015892,0 -2.03204,0.38767 -2.807065,1.16272 l -9.312609,9.31261 c -1.55009,1.55005 -1.55009,4.06304 0,5.61309 l 32.422309,32.42386 -32.422309,32.42386 c -1.549791,1.54928 -1.549791,4.06118 -0.0016,5.61155 l 9.314159,9.31416 c 1.5495,1.55057 4.062999,1.5516 5.613611,0.002 l 32.423343,-32.42541 32.42386,32.42541 c 1.55061,1.5496 4.06411,1.54857 5.61361,-0.002 l 9.31416,-9.31416 c 1.54831,-1.55038 1.54727,-4.06227 -0.002,-5.61155 L 95.605211,164.70883 128.027,132.28497 c 1.55009,-1.55005 1.55009,-4.06304 0,-5.61309 l -9.31209,-9.31261 c -0.73003,-0.73002 -1.71586,-1.14701 -2.74815,-1.16221 -1.0729,-0.0158 -2.10672,0.40347 -2.86546,1.16221 L 80.67744,149.78313 48.254097,117.35927 c -0.775025,-0.77505 -1.790657,-1.16272 -2.806546,-1.16272 z" />
<g
id="g22" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.6 KiB

View file

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Before After
Before After

View file

@ -0,0 +1,125 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20)"
sodipodi:docname="U+232B Erase To the Left.svg"
width="1000"
height="1000"
viewBox="0 0 264.58333 264.58334"
version="1.1"
id="svg964"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<sodipodi:namedview
inkscape:document-rotation="0"
inkscape:snap-global="true"
inkscape:current-layer="layer1"
inkscape:window-maximized="1"
inkscape:window-y="32"
inkscape:window-x="0"
inkscape:cy="432.54171"
inkscape:cx="961.39837"
inkscape:zoom="0.5710432"
inkscape:guide-bbox="true"
showguides="true"
showgrid="false"
id="namedview8"
inkscape:window-height="1011"
inkscape:window-width="1920"
inkscape:pageshadow="2"
inkscape:pageopacity="0"
guidetolerance="10"
gridtolerance="10"
objecttolerance="10"
borderopacity="1"
bordercolor="#666666"
pagecolor="#ffffff"
inkscape:pagecheckerboard="0">
<sodipodi:guide
inkscape:color="rgb(0,0,255)"
inkscape:label=""
inkscape:locked="false"
id="guide815"
orientation="0,1"
position="0,52.916667" />
<sodipodi:guide
inkscape:color="rgb(0,0,255)"
inkscape:label=""
inkscape:locked="false"
id="guide817"
orientation="1,0"
position="5.2916667,0" />
<sodipodi:guide
inkscape:color="rgb(0,0,255)"
inkscape:label=""
inkscape:locked="false"
id="guide819"
orientation="0,1"
position="0,259.29167" />
<sodipodi:guide
inkscape:color="rgb(0,0,255)"
inkscape:label=""
inkscape:locked="false"
id="guide821"
orientation="1,0"
position="259.29167,0" />
<sodipodi:guide
inkscape:color="rgb(0,0,255)"
inkscape:label=""
inkscape:locked="false"
id="guide823"
orientation="1,0"
position="29.104167,0" />
<sodipodi:guide
inkscape:color="rgb(0,0,255)"
inkscape:label=""
inkscape:locked="false"
id="guide825"
orientation="1,0"
position="235.47917,0" />
<sodipodi:guide
inkscape:color="rgb(0,0,255)"
inkscape:label=""
inkscape:locked="false"
id="guide827"
orientation="0,1"
position="132.29167,161.39583" />
<sodipodi:guide
inkscape:color="rgb(0,0,255)"
inkscape:label=""
inkscape:locked="false"
id="guide829"
orientation="1,0"
position="132.29167,161.39583" />
</sodipodi:namedview>
<defs
id="defs958" />
<metadata
id="metadata961">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-32.41665)">
<path
id="path841"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:30;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="M 420.04883 165.20508 C 416.51221 165.13217 413.06407 166.31022 410.3125 168.5332 L 25.498047 488.33203 C 18.069348 494.3363 18.069348 505.66174 25.498047 511.66602 L 410.3125 831.46484 C 413.39833 833.95684 416.95828 834.96942 420.39062 834.80078 L 964.99609 834.80078 C 973.28002 834.79995 979.9953 828.08471 979.99609 819.80078 L 979.99609 565.06836 L 979.99609 434.9375 L 979.99609 180.20508 C 979.9953 171.92115 973.28002 165.20591 964.99609 165.20508 L 420.04883 165.20508 z M 828.15234 316.64844 C 831.99193 316.64844 835.83249 318.11366 838.76172 321.04297 L 873.95898 356.24023 C 879.81759 362.09869 879.81759 371.59662 873.95898 377.45508 L 751.41797 500.00195 L 873.95898 622.54883 C 879.81646 628.40437 879.81646 637.89815 873.96484 643.75781 L 838.76172 678.96094 C 832.90534 684.82136 823.4055 684.82528 817.54492 678.96875 L 695 556.41602 L 572.45312 678.96875 C 566.59255 684.82551 557.09271 684.82159 551.23633 678.96094 L 516.0332 643.75781 C 510.18132 637.89811 510.18524 628.40437 516.04102 622.54883 L 638.58008 500.00195 L 516.04102 377.45508 C 510.18241 371.59662 510.18241 362.09869 516.04102 356.24023 L 551.23633 321.04297 C 553.9955 318.28384 557.72148 316.70784 561.62305 316.65039 C 565.6781 316.59052 569.58545 318.17529 572.45312 321.04297 L 695 443.58984 L 817.54492 321.04297 C 820.47415 318.11366 824.31276 316.64844 828.15234 316.64844 z "
transform="matrix(0.26458333,0,0,0.26458333,0,32.41665)" />
<g
id="g22" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.9 KiB

View file

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

Before After
Before After

View file

@ -0,0 +1,122 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
inkscape:version="1.0 (4035a4fb49, 2020-05-01)"
sodipodi:docname="U+2B60 Leftwards Triangle-Headed Arrow .svg"
width="1000"
height="1000"
viewBox="0 0 264.58333 264.58334"
version="1.1"
id="svg964">
<sodipodi:namedview
inkscape:document-rotation="0"
inkscape:snap-global="false"
inkscape:current-layer="layer1"
inkscape:window-maximized="1"
inkscape:window-y="27"
inkscape:window-x="0"
inkscape:cy="398.16955"
inkscape:cx="369.80754"
inkscape:zoom="0.5710432"
inkscape:guide-bbox="true"
showguides="true"
showgrid="false"
id="namedview8"
inkscape:window-height="1016"
inkscape:window-width="1920"
inkscape:pageshadow="2"
inkscape:pageopacity="0"
guidetolerance="10"
gridtolerance="10"
objecttolerance="10"
borderopacity="1"
bordercolor="#666666"
pagecolor="#ffffff">
<sodipodi:guide
inkscape:color="rgb(0,0,255)"
inkscape:label=""
inkscape:locked="false"
id="guide815"
orientation="0,1"
position="0,52.916667" />
<sodipodi:guide
inkscape:color="rgb(0,0,255)"
inkscape:label=""
inkscape:locked="false"
id="guide817"
orientation="1,0"
position="5.2916667,0" />
<sodipodi:guide
inkscape:color="rgb(0,0,255)"
inkscape:label=""
inkscape:locked="false"
id="guide819"
orientation="0,1"
position="0,259.29167" />
<sodipodi:guide
inkscape:color="rgb(0,0,255)"
inkscape:label=""
inkscape:locked="false"
id="guide821"
orientation="1,0"
position="259.29167,0" />
<sodipodi:guide
inkscape:color="rgb(0,0,255)"
inkscape:label=""
inkscape:locked="false"
id="guide823"
orientation="1,0"
position="29.104167,0" />
<sodipodi:guide
inkscape:color="rgb(0,0,255)"
inkscape:label=""
inkscape:locked="false"
id="guide825"
orientation="1,0"
position="235.47917,0" />
<sodipodi:guide
inkscape:color="rgb(0,0,255)"
inkscape:label=""
inkscape:locked="false"
id="guide827"
orientation="0,1"
position="132.29167,161.39583" />
<sodipodi:guide
inkscape:color="rgb(0,0,255)"
inkscape:label=""
inkscape:locked="false"
id="guide829"
orientation="1,0"
position="132.29167,161.39583" />
</sodipodi:namedview>
<defs
id="defs958" />
<metadata
id="metadata961">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-32.41665)">
<path
sodipodi:nodetypes="cccccccccccccccc"
d="m 111.13772,76.12716 c 2.15968,0.04486 3.88735,1.808597 3.8871,3.96875 v 59.46045 c 2.2e-4,2.19179 1.77696,3.96853 3.96875,3.96875 h 136.32811 c 2.19179,2.2e-4 3.96853,1.77696 3.96875,3.96875 v 34.43046 c -2.2e-4,2.19179 -1.77696,3.96854 -3.96875,3.96875 H 118.99357 c -2.19179,2.2e-4 -3.96853,1.77696 -3.96875,3.96875 v 59.46045 c -0.002,3.33572 -3.86802,5.18189 -6.46317,3.08612 L 6.7461621,167.79495 c -1.9655102,-1.58863 -1.9655102,-4.58516 0,-6.17379 L 108.56165,77.007725 c 0.72802,-0.588163 1.64034,-0.899858 2.57607,-0.880565 z"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:7.9375;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
id="path841" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.1 KiB

View file

@ -0,0 +1,123 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
inkscape:version="1.0 (4035a4fb49, 2020-05-01)"
sodipodi:docname="U+2B95 Rightwards Black Arrow .svg"
width="1000"
height="1000"
viewBox="0 0 264.58333 264.58334"
version="1.1"
id="svg964">
<sodipodi:namedview
inkscape:document-rotation="0"
inkscape:snap-global="false"
inkscape:current-layer="layer1"
inkscape:window-maximized="1"
inkscape:window-y="27"
inkscape:window-x="0"
inkscape:cy="398.16955"
inkscape:cx="369.80754"
inkscape:zoom="0.5710432"
inkscape:guide-bbox="true"
showguides="true"
showgrid="false"
id="namedview8"
inkscape:window-height="1016"
inkscape:window-width="1920"
inkscape:pageshadow="2"
inkscape:pageopacity="0"
guidetolerance="10"
gridtolerance="10"
objecttolerance="10"
borderopacity="1"
bordercolor="#666666"
pagecolor="#ffffff">
<sodipodi:guide
inkscape:color="rgb(0,0,255)"
inkscape:label=""
inkscape:locked="false"
id="guide815"
orientation="0,1"
position="0,52.916667" />
<sodipodi:guide
inkscape:color="rgb(0,0,255)"
inkscape:label=""
inkscape:locked="false"
id="guide817"
orientation="1,0"
position="5.2916667,0" />
<sodipodi:guide
inkscape:color="rgb(0,0,255)"
inkscape:label=""
inkscape:locked="false"
id="guide819"
orientation="0,1"
position="0,259.29167" />
<sodipodi:guide
inkscape:color="rgb(0,0,255)"
inkscape:label=""
inkscape:locked="false"
id="guide821"
orientation="1,0"
position="259.29167,0" />
<sodipodi:guide
inkscape:color="rgb(0,0,255)"
inkscape:label=""
inkscape:locked="false"
id="guide823"
orientation="1,0"
position="29.104167,0" />
<sodipodi:guide
inkscape:color="rgb(0,0,255)"
inkscape:label=""
inkscape:locked="false"
id="guide825"
orientation="1,0"
position="235.47917,0" />
<sodipodi:guide
inkscape:color="rgb(0,0,255)"
inkscape:label=""
inkscape:locked="false"
id="guide827"
orientation="0,1"
position="132.29167,161.39583" />
<sodipodi:guide
inkscape:color="rgb(0,0,255)"
inkscape:label=""
inkscape:locked="false"
id="guide829"
orientation="1,0"
position="132.29167,161.39583" />
</sodipodi:namedview>
<defs
id="defs958" />
<metadata
id="metadata961">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-32.41665)">
<path
sodipodi:nodetypes="cccccccccccccccc"
transform="matrix(0.26458333,0,0,0.26458333,0,32.41665)"
d="m 579.87305,165.20508 c -8.16257,0.16955 -14.69235,6.83564 -14.69141,15 V 404.9375 c -8.3e-4,8.28393 -6.71607,14.99917 -15,15 H 34.925781 c -8.283925,8.3e-4 -14.999164,6.71607 -15,15 v 130.13086 c 8.36e-4,8.28393 6.716075,14.99921 15,15 H 550.18164 c 8.28393,8.3e-4 14.99917,6.71607 15,15 v 224.73242 c 0.008,12.60745 14.61929,19.58508 24.42774,11.66406 L 974.42383,511.66602 c 7.4287,-6.00428 7.4287,-17.32972 0,-23.33399 L 589.60938,168.5332 c -2.75158,-2.22298 -6.19972,-3.40104 -9.73633,-3.32812 z"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:30;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
id="path841" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.1 KiB

View file

@ -0,0 +1,122 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
id="svg964"
version="1.1"
viewBox="0 0 264.58333 264.58334"
height="1000"
width="1000"
sodipodi:docname="U+2B70 Leftwards Triangle-Headed Arrow To Bar.svg"
inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1011"
id="namedview8"
showgrid="false"
showguides="true"
inkscape:guide-bbox="true"
inkscape:zoom="0.78434916"
inkscape:cx="345.50939"
inkscape:cy="525.27627"
inkscape:window-x="0"
inkscape:window-y="32"
inkscape:window-maximized="1"
inkscape:current-layer="layer1"
inkscape:snap-global="true"
inkscape:pagecheckerboard="0">
<sodipodi:guide
position="0,52.916667"
orientation="0,1"
id="guide815"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
<sodipodi:guide
position="5.2916667,0"
orientation="1,0"
id="guide817"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
<sodipodi:guide
position="0,259.29167"
orientation="0,1"
id="guide819"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
<sodipodi:guide
position="259.29167,0"
orientation="1,0"
id="guide821"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
<sodipodi:guide
position="29.104167,0"
orientation="1,0"
id="guide823"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
<sodipodi:guide
position="235.47917,0"
orientation="1,0"
id="guide825"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
<sodipodi:guide
position="132.29167,161.39583"
orientation="0,1"
id="guide827"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
<sodipodi:guide
position="132.29167,161.39583"
orientation="1,0"
id="guide829"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
</sodipodi:namedview>
<defs
id="defs958" />
<metadata
id="metadata961">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
transform="translate(0,-32.41665)"
id="layer1"
inkscape:groupmode="layer">
<path
id="path832-3"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:30;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="M 35.107422 165.20508 C 26.823491 165.20591 20.108216 171.92115 20.107422 180.20508 L 20.107422 819.80078 C 20.108216 828.08471 26.823491 834.79995 35.107422 834.80078 L 165.23828 834.80078 C 173.52221 834.79995 180.23745 828.08471 180.23828 819.80078 L 180.23828 519.27539 L 555.63672 831.46484 C 565.44512 839.38586 580.05807 832.40823 580.06445 819.80078 L 580.06445 595.06836 C 580.06528 586.78443 586.78052 580.06919 595.06445 580.06836 L 964.99609 580.06836 C 973.28002 580.06757 979.99526 573.35229 979.99609 565.06836 L 979.99609 434.9375 C 979.99526 426.65357 973.28002 419.93833 964.99609 419.9375 L 595.06445 419.9375 C 586.78052 419.93667 580.06528 413.22143 580.06445 404.9375 L 580.06445 180.20508 C 580.06347 171.74797 573.07454 164.96209 564.62109 165.21094 C 561.34398 165.30679 558.18786 166.474 555.63672 168.5332 L 180.23828 480.72266 L 180.23828 180.20508 C 180.23745 171.92115 173.52221 165.20591 165.23828 165.20508 L 35.107422 165.20508 z "
transform="matrix(0.26458333,0,0,0.26458333,0,32.41665)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.5 KiB

View file

@ -0,0 +1,120 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
id="svg964"
version="1.1"
viewBox="0 0 264.58333 264.58334"
height="1000"
width="1000"
sodipodi:docname="U+2B72 Rightwards Triangle-Headed Arrow To Bar.svg"
inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1011"
id="namedview8"
showgrid="false"
showguides="true"
inkscape:guide-bbox="true"
inkscape:zoom="0.78434916"
inkscape:cx="345.50939"
inkscape:cy="326.38525"
inkscape:window-x="0"
inkscape:window-y="32"
inkscape:window-maximized="1"
inkscape:current-layer="layer1"
inkscape:snap-global="true"
inkscape:pagecheckerboard="0">
<sodipodi:guide
position="0,52.916667"
orientation="0,1"
id="guide815"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
<sodipodi:guide
position="5.2916667,0"
orientation="1,0"
id="guide817"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
<sodipodi:guide
position="0,259.29167"
orientation="0,1"
id="guide819"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
<sodipodi:guide
position="259.29167,0"
orientation="1,0"
id="guide821"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
<sodipodi:guide
position="29.104167,0"
orientation="1,0"
id="guide823"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
<sodipodi:guide
position="235.47917,0"
orientation="1,0"
id="guide825"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
<sodipodi:guide
position="132.29167,161.39583"
orientation="0,1"
id="guide827"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
<sodipodi:guide
position="132.29167,161.39583"
orientation="1,0"
id="guide829"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
</sodipodi:namedview>
<defs
id="defs958" />
<metadata
id="metadata961">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<g
transform="translate(0,-32.41665)"
id="layer1"
inkscape:groupmode="layer">
<path
id="path832-3"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:7.9375;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 255.32188,76.12716 c 2.19179,2.2e-4 3.96854,1.77696 3.96875,3.96875 v 169.22636 c -2.1e-4,2.19179 -1.77696,3.96853 -3.96875,3.96875 h -34.43046 c -2.19179,-2.2e-4 -3.96853,-1.77696 -3.96875,-3.96875 v -79.51401 l -99.32417,82.60013 c -2.59514,2.09577 -6.46148,0.2496 -6.46317,-3.08612 v -59.46045 c -2.2e-4,-2.19179 -1.77696,-3.96853 -3.96875,-3.96875 H 9.2888386 c -2.1917898,-2.1e-4 -3.9685303,-1.77696 -3.9687499,-3.96875 v -34.43046 c 2.196e-4,-2.19179 1.7769601,-3.96853 3.9687499,-3.96875 H 107.16658 c 2.19179,-2.2e-4 3.96853,-1.77696 3.96875,-3.96875 V 80.09591 c 2.6e-4,-2.23761 1.84942,-4.033041 4.08606,-3.967199 0.86707,0.02536 1.70212,0.334184 2.37711,0.879014 l 99.32417,82.600125 V 80.09591 c 2.2e-4,-2.19179 1.77696,-3.96853 3.96875,-3.96875 z" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.2 KiB

View file

@ -0,0 +1,134 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20)"
sodipodi:docname="U+2B82 Rightwards Triangle-Headed Arrow Over Leftwards Triangle-Headed Arrow.svg"
width="1000"
height="1000"
viewBox="0 0 264.58333 264.58334"
version="1.1"
id="svg964"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<sodipodi:namedview
inkscape:document-rotation="0"
inkscape:snap-global="true"
inkscape:current-layer="layer1"
inkscape:window-maximized="1"
inkscape:window-y="32"
inkscape:window-x="0"
inkscape:cy="440.81976"
inkscape:cx="627.83421"
inkscape:zoom="0.59888422"
inkscape:guide-bbox="true"
showguides="true"
showgrid="false"
id="namedview8"
inkscape:window-height="1011"
inkscape:window-width="1920"
inkscape:pageshadow="2"
inkscape:pageopacity="0"
guidetolerance="10"
gridtolerance="10"
objecttolerance="10"
borderopacity="1"
bordercolor="#666666"
pagecolor="#ffffff"
inkscape:pagecheckerboard="0"
inkscape:snap-page="false"
inkscape:snap-to-guides="true"
inkscape:snap-grids="false"
inkscape:snap-others="true"
inkscape:snap-nodes="true"
inkscape:snap-bbox="true">
<sodipodi:guide
inkscape:color="rgb(0,0,255)"
inkscape:label=""
inkscape:locked="false"
id="guide815"
orientation="0,1"
position="0,52.916667" />
<sodipodi:guide
inkscape:color="rgb(0,0,255)"
inkscape:label=""
inkscape:locked="false"
id="guide817"
orientation="1,0"
position="5.2916667,0" />
<sodipodi:guide
inkscape:color="rgb(0,0,255)"
inkscape:label=""
inkscape:locked="false"
id="guide819"
orientation="0,1"
position="0,259.29167" />
<sodipodi:guide
inkscape:color="rgb(0,0,255)"
inkscape:label=""
inkscape:locked="false"
id="guide821"
orientation="1,0"
position="259.29167,0" />
<sodipodi:guide
inkscape:color="rgb(0,0,255)"
inkscape:label=""
inkscape:locked="false"
id="guide823"
orientation="1,0"
position="29.104167,0" />
<sodipodi:guide
inkscape:color="rgb(0,0,255)"
inkscape:label=""
inkscape:locked="false"
id="guide825"
orientation="1,0"
position="235.47917,0" />
<sodipodi:guide
inkscape:color="rgb(0,0,255)"
inkscape:label=""
inkscape:locked="false"
id="guide827"
orientation="0,1"
position="132.29167,161.39583" />
<sodipodi:guide
inkscape:color="rgb(0,0,255)"
inkscape:label=""
inkscape:locked="false"
id="guide829"
orientation="1,0"
position="132.29167,161.39583" />
</sodipodi:namedview>
<defs
id="defs958" />
<metadata
id="metadata961">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-32.41665)">
<path
sodipodi:nodetypes="cccccccccccccccc"
d="m 174.5914,50.361898 c -2.15968,0.04486 -3.88735,1.808597 -3.8871,3.96875 v 48.877122 c -2.2e-4,2.19179 -1.77696,3.96853 -3.96875,3.96875 H 9.2407794 c -2.1917884,2.2e-4 -3.9685287,1.77696 -3.9687499,3.96875 v 34.43045 c 2.212e-4,2.19179 1.7769615,3.96854 3.9687499,3.96875 H 166.73555 c 2.19179,2.2e-4 3.96853,1.77696 3.96875,3.96875 v 48.87712 c 0.002,3.33572 3.86802,5.18189 6.46317,3.08612 l 80.64883,-74.03011 c 1.96551,-1.58863 1.96551,-4.58515 0,-6.17378 L 177.16747,51.242464 c -0.72802,-0.588164 -1.64034,-0.899859 -2.57607,-0.880566 z"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:7.9375;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
id="path841" />
<path
id="path16"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:30;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="M 181.25391 482.71289 L 25.498047 625.6875 C 18.069348 631.69177 18.069348 643.01721 25.498047 649.02148 L 330.3125 928.82031 C 340.12095 936.74133 354.73268 929.7637 354.74023 917.15625 L 354.74023 732.42383 C 354.74107 724.1399 361.4563 717.42466 369.74023 717.42383 L 964.99609 717.42383 C 973.28002 717.42303 979.99526 710.70776 979.99609 702.42383 L 979.99609 572.29297 C 979.99526 564.00904 973.28002 557.2938 964.99609 557.29297 L 834.09375 557.29297 C 808.85644 580.47789 783.62321 603.66677 758.38086 626.84766 C 738.58721 644.94751 718.93574 663.20661 699.09375 681.25 C 689.39662 689.8341 677.42827 696.51296 664.28516 697.35156 C 637.29619 699.42218 609.8368 678.92 606.08203 651.73828 C 605.3507 620.26206 605.20414 588.77752 605.19727 557.29297 L 369.74023 557.29297 C 361.4563 557.29214 354.74107 550.5769 354.74023 542.29297 L 354.74023 482.73242 C 296.9114 482.72628 239.08275 482.71891 181.25391 482.71289 z "
transform="matrix(0.26458333,0,0,0.26458333,0,32.41665)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.6 KiB

View file

@ -0,0 +1,127 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20)"
sodipodi:docname="U+2190 Leftwards Arrow (Kopie).svg"
width="1000"
height="1000"
viewBox="0 0 264.58333 264.58334"
version="1.1"
id="svg964"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<sodipodi:namedview
inkscape:document-rotation="0"
inkscape:snap-global="true"
inkscape:current-layer="layer1"
inkscape:window-maximized="1"
inkscape:window-y="32"
inkscape:window-x="0"
inkscape:cy="443.69979"
inkscape:cx="850.06562"
inkscape:zoom="0.69641682"
inkscape:guide-bbox="true"
showguides="true"
showgrid="false"
id="namedview8"
inkscape:window-height="1011"
inkscape:window-width="1920"
inkscape:pageshadow="2"
inkscape:pageopacity="0"
guidetolerance="10"
gridtolerance="10"
objecttolerance="10"
borderopacity="1"
bordercolor="#666666"
pagecolor="#ffffff"
inkscape:pagecheckerboard="0"
inkscape:snap-bbox="true"
inkscape:bbox-paths="false"
inkscape:bbox-nodes="true">
<sodipodi:guide
inkscape:color="rgb(0,0,255)"
inkscape:label=""
inkscape:locked="false"
id="guide815"
orientation="0,1"
position="0,52.916667" />
<sodipodi:guide
inkscape:color="rgb(0,0,255)"
inkscape:label=""
inkscape:locked="false"
id="guide817"
orientation="1,0"
position="5.2916667,0" />
<sodipodi:guide
inkscape:color="rgb(0,0,255)"
inkscape:label=""
inkscape:locked="false"
id="guide819"
orientation="0,1"
position="0,259.29167" />
<sodipodi:guide
inkscape:color="rgb(0,0,255)"
inkscape:label=""
inkscape:locked="false"
id="guide821"
orientation="1,0"
position="259.29167,0" />
<sodipodi:guide
inkscape:color="rgb(0,0,255)"
inkscape:label=""
inkscape:locked="false"
id="guide823"
orientation="1,0"
position="29.104167,0" />
<sodipodi:guide
inkscape:color="rgb(0,0,255)"
inkscape:label=""
inkscape:locked="false"
id="guide825"
orientation="1,0"
position="235.47917,0" />
<sodipodi:guide
inkscape:color="rgb(0,0,255)"
inkscape:label=""
inkscape:locked="false"
id="guide827"
orientation="0,1"
position="132.29167,161.39583" />
<sodipodi:guide
inkscape:color="rgb(0,0,255)"
inkscape:label=""
inkscape:locked="false"
id="guide829"
orientation="1,0"
position="132.29167,161.39583" />
</sodipodi:namedview>
<defs
id="defs958" />
<metadata
id="metadata961">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-32.41665)">
<path
id="path841"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:30;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 834.86914,20 c -8.28393,8.31e-4 -14.99921,6.716069 -15,15 v 369.9375 c -8.7e-4,8.28393 -6.71607,14.99917 -15,15 H 449.74023 c -8.28393,-8.3e-4 -14.99916,-6.71607 -15,-15 V 180.20508 c 9.5e-4,-8.16436 -6.52883,-14.83045 -14.6914,-15 -3.53662,-0.0729 -6.98476,1.10514 -9.73633,3.32812 L 25.498047,488.33203 c -7.4287,6.00427 -7.4287,17.32971 0,23.33399 L 410.3125,831.46484 c 9.80844,7.92102 24.42018,0.94339 24.42773,-11.66406 V 595.06836 c 8.4e-4,-8.28393 6.71607,-14.99917 15,-15 h 515.25586 c 8.28393,-7.9e-4 14.99917,-6.71607 15,-15 L 980,35 c -8.3e-4,-8.283931 -6.71607,-14.999169 -15,-15 z"
transform="matrix(0.26458333,0,0,0.26458333,0,32.41665)"
sodipodi:nodetypes="cccccccccccccccccccc" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.3 KiB

View file

@ -0,0 +1,159 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20)"
sodipodi:docname="U+2B60 Leftwards Triangle-Headed Arrow (Kopie).svg"
width="1000"
height="1000"
viewBox="0 0 264.58333 264.58334"
version="1.1"
id="svg964"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<sodipodi:namedview
inkscape:document-rotation="0"
inkscape:snap-global="true"
inkscape:current-layer="layer1"
inkscape:window-maximized="1"
inkscape:window-y="32"
inkscape:window-x="0"
inkscape:cy="437.77147"
inkscape:cx="266.98483"
inkscape:zoom="0.71726922"
inkscape:guide-bbox="true"
showguides="true"
showgrid="false"
id="namedview8"
inkscape:window-height="1011"
inkscape:window-width="1920"
inkscape:pageshadow="2"
inkscape:pageopacity="0"
guidetolerance="10"
gridtolerance="10"
objecttolerance="10"
borderopacity="1"
bordercolor="#666666"
pagecolor="#ffffff"
inkscape:pagecheckerboard="0"
inkscape:snap-bbox="false">
<sodipodi:guide
inkscape:color="rgb(0,0,255)"
inkscape:label=""
inkscape:locked="false"
id="guide815"
orientation="0,1"
position="0,52.916667" />
<sodipodi:guide
inkscape:color="rgb(0,0,255)"
inkscape:label=""
inkscape:locked="false"
id="guide817"
orientation="1,0"
position="5.2916667,0" />
<sodipodi:guide
inkscape:color="rgb(0,0,255)"
inkscape:label=""
inkscape:locked="false"
id="guide819"
orientation="0,1"
position="0,259.29167" />
<sodipodi:guide
inkscape:color="rgb(0,0,255)"
inkscape:label=""
inkscape:locked="false"
id="guide821"
orientation="1,0"
position="259.29167,0" />
<sodipodi:guide
inkscape:color="rgb(0,0,255)"
inkscape:label=""
inkscape:locked="false"
id="guide823"
orientation="1,0"
position="29.104167,0" />
<sodipodi:guide
inkscape:color="rgb(0,0,255)"
inkscape:label=""
inkscape:locked="false"
id="guide825"
orientation="1,0"
position="235.47917,0" />
<sodipodi:guide
inkscape:color="rgb(0,0,255)"
inkscape:label=""
inkscape:locked="false"
id="guide827"
orientation="0,1"
position="132.29167,161.39583" />
<sodipodi:guide
inkscape:color="rgb(0,0,255)"
inkscape:label=""
inkscape:locked="false"
id="guide829"
orientation="1,0"
position="132.29167,161.39583" />
</sodipodi:namedview>
<defs
id="defs958">
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect6088"
is_visible="true"
lpeversion="1"
satellites_param="F,0,0,1,0,38,0,4 @ F,0,0,1,0,38,0,4 @ F,0,0,1,0,38,0,4"
unit="px"
method="auto"
mode="F"
radius="38"
chamfer_steps="4"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect5304"
is_visible="true"
lpeversion="1"
satellites_param="F,0,0,1,0,7,0,1 @ F,0,0,1,0,7,0,1 @ F,0,0,1,0,7,0,1"
unit="px"
method="auto"
mode="F"
radius="7"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
</defs>
<metadata
id="metadata961">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-32.41665)">
<path
id="path2476"
inkscape:transform-center-x="26.879406"
d="M 204.85638,252.74656 55.648672,168.16048 a 3.9683103,3.9683103 90.000003 0 1 0,-6.90434 L 204.85639,76.670075 a 4.0782719,4.0782719 30.225521 0 1 6.08954,3.54783 l -1e-5,168.980825 a 4.078272,4.078272 149.77449 0 1 -6.08954,3.54783 z"
style="fill:#000000;stroke-width:0.273459" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.6 KiB

1091
icon-font/icon.sfd Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,742 +0,0 @@
SplineFontDB: 3.2
FontName: Icon
FullName: Icon
FamilyName: Icon
Weight: Regular
Copyright: Copyright (c) 2018 Unrud <unrud@outlook.com>
UComments: "2018-8-29: Created with FontForge (http://fontforge.org)"
Version: 001.000
ItalicAngle: 0
UnderlinePosition: -100
UnderlineWidth: 50
Ascent: 800
Descent: 200
InvalidEm: 0
LayerCount: 2
Layer: 0 0 "Back" 1
Layer: 1 0 "Zeichen" 0
XUID: [1021 32 396054688 5708680]
FSType: 0
OS2Version: 0
OS2_WeightWidthSlopeOnly: 0
OS2_UseTypoMetrics: 1
CreationTime: 1535548659
ModificationTime: 1597866573
PfmFamily: 17
TTFWeight: 400
TTFWidth: 5
LineGap: 90
VLineGap: 0
OS2TypoAscent: 0
OS2TypoAOffset: 1
OS2TypoDescent: 0
OS2TypoDOffset: 1
OS2TypoLinegap: 90
OS2WinAscent: 0
OS2WinAOffset: 1
OS2WinDescent: 0
OS2WinDOffset: 1
HheadAscent: 0
HheadAOffset: 1
HheadDescent: 0
HheadDOffset: 1
OS2Vendor: 'PfEd'
MarkAttachClasses: 1
DEI: 91125
LangName: 1033
Encoding: UnicodeFull
Compacted: 1
UnicodeInterp: none
NameList: AGL For New Fonts
DisplaySize: -48
AntiAlias: 1
FitToEm: 0
WinInfo: 0 35 14
BeginPrivate: 0
EndPrivate
TeXData: 1 0 0 346030 173015 115343 0 1048576 115343 783286 444596 497025 792723 393216 433062 380633 303038 157286 324010 404750 52429 2506097 1059062 262144
BeginChars: 1114112 14
StartChar: uni2328
Encoding: 9000 9000 0
Width: 1000
VWidth: 0
HStem: 10 80<250 750> 200 60<285 395 445 555 605 715> 370 50<125 235 285 395 445 555 605 715 765 875> 530 60<125 235 285 395 445 555 605 715 765 875>
VStem: 20 105<260 370 420 530> 235 50<260 370 420 530> 395 50<260 370 420 530> 555 50<260 370 420 530> 715 50<260 370 420 530> 750 230<90 200> 875 105<260 370 420 530>
LayerCount: 2
Fore
SplineSet
70 590 m 2xff20
930 590 l 2
957.700195312 590 980 567.700195312 980 540 c 2
980 60 l 2
980 32.2998046875 957.700195312 10 930 10 c 2
70 10 l 2
42.2998046875 10 20 32.2998046875 20 60 c 2
20 540 l 2
20 567.700195312 42.2998046875 590 70 590 c 2xff20
135 530 m 2
129.459960938 530 125 525.540039062 125 520 c 2
125 430 l 2
125 424.459960938 129.459960938 420 135 420 c 2
225 420 l 2
230.540039062 420 235 424.459960938 235 430 c 2
235 520 l 2
235 525.540039062 230.540039062 530 225 530 c 2
135 530 l 2
295 530 m 2
289.459960938 530 285 525.540039062 285 520 c 2
285 430 l 2
285 424.459960938 289.459960938 420 295 420 c 2
385 420 l 2
390.540039062 420 395 424.459960938 395 430 c 2
395 520 l 2
395 525.540039062 390.540039062 530 385 530 c 2
295 530 l 2
455 530 m 2
449.459960938 530 445 525.540039062 445 520 c 2
445 430 l 2
445 424.459960938 449.459960938 420 455 420 c 2
545 420 l 2
550.540039062 420 555 424.459960938 555 430 c 2
555 520 l 2
555 525.540039062 550.540039062 530 545 530 c 2
455 530 l 2
615 530 m 2
609.459960938 530 605 525.540039062 605 520 c 2
605 430 l 2
605 424.459960938 609.459960938 420 615 420 c 2
705 420 l 2
710.540039062 420 715 424.459960938 715 430 c 2
715 520 l 2xff80
715 525.540039062 710.540039062 530 705 530 c 2
615 530 l 2
775 530 m 2
769.459960938 530 765 525.540039062 765 520 c 2
765 430 l 2
765 424.459960938 769.459960938 420 775 420 c 2
865 420 l 2
870.540039062 420 875 424.459960938 875 430 c 2
875 520 l 2xffa0
875 525.540039062 870.540039062 530 865 530 c 2
775 530 l 2
135 370 m 2
129.459960938 370 125 365.540039062 125 360 c 2
125 270 l 2
125 264.459960938 129.459960938 260 135 260 c 2
225 260 l 2
230.540039062 260 235 264.459960938 235 270 c 2
235 360 l 2
235 365.540039062 230.540039062 370 225 370 c 2
135 370 l 2
295 370 m 2
289.459960938 370 285 365.540039062 285 360 c 2
285 270 l 2
285 264.459960938 289.459960938 260 295 260 c 2
385 260 l 2
390.540039062 260 395 264.459960938 395 270 c 2
395 360 l 2
395 365.540039062 390.540039062 370 385 370 c 2
295 370 l 2
455 370 m 2
449.459960938 370 445 365.540039062 445 360 c 2
445 270 l 2
445 264.459960938 449.459960938 260 455 260 c 2
545 260 l 2
550.540039062 260 555 264.459960938 555 270 c 2
555 360 l 2
555 365.540039062 550.540039062 370 545 370 c 2
455 370 l 2
615 370 m 2
609.459960938 370 605 365.540039062 605 360 c 2
605 270 l 2
605 264.459960938 609.459960938 260 615 260 c 2
705 260 l 2
710.540039062 260 715 264.459960938 715 270 c 2
715 360 l 2
715 365.540039062 710.540039062 370 705 370 c 2
615 370 l 2
775 370 m 2
769.459960938 370 765 365.540039062 765 360 c 2
765 270 l 2
765 264.459960938 769.459960938 260 775 260 c 2
865 260 l 2
870.540039062 260 875 264.459960938 875 270 c 2
875 360 l 2
875 365.540039062 870.540039062 370 865 370 c 2
775 370 l 2
260 200 m 2
254.459960938 200 250 195.540039062 250 190 c 2
250 100 l 2
250 94.4599609375 254.459960938 90 260 90 c 2
740 90 l 2
745.540039062 90 750 94.4599609375 750 100 c 2
750 190 l 2xff40
750 195.540039062 745.540039062 200 740 200 c 2
260 200 l 2
EndSplineSet
Validated: 1
EndChar
StartChar: uni2922
Encoding: 10530 10530 1
Width: 1000
VWidth: 0
HStem: -90.0059 80.1387<246.65 410.016> 609.867 80.1387<589.938 753.304>
VStem: 109.941 80.1387<46.7031 210.068> 809.874 80.1387<389.932 553.297>
LayerCount: 2
Fore
SplineSet
599.938476562 690.005859375 m 2
880.030273438 690.005859375 l 2
885.454101562 690.005859375 889.83203125 685.727539062 890.012695312 680.34765625 c 2
890.012695312 399.931640625 l 2
890.012695312 394.391601562 885.552734375 389.931640625 880.012695312 389.931640625 c 2
819.874023438 389.931640625 l 2
814.333984375 389.931640625 809.874023438 394.391601562 809.874023438 399.931640625 c 2
809.874023438 553.296875 l 1
635.295898438 378.71484375 l 2
631.377929688 374.799804688 625.073242188 374.80078125 621.157226562 378.716796875 c 2
578.723632812 421.150390625 l 2
574.806640625 425.067382812 574.805664062 431.372070312 578.721679688 435.2890625 c 2
578.723632812 435.291015625 l 1
753.303710938 609.8671875 l 1
599.543945312 609.8671875 l 2
594.189453125 610.075195312 589.938476562 614.44140625 589.938476562 619.84765625 c 2
589.938476562 680.005859375 l 2
589.938476562 685.545898438 594.3984375 690.005859375 599.938476562 690.005859375 c 2
400.015625 -90.005859375 m 2
119.923828125 -90.005859375 l 2
114.499023438 -90.005859375 110.122070312 -85.7275390625 109.94140625 -80.34765625 c 2
109.94140625 200.068359375 l 2
109.94140625 205.608398438 114.401367188 210.068359375 119.94140625 210.068359375 c 2
180.080078125 210.068359375 l 2
185.620117188 210.068359375 190.080078125 205.608398438 190.080078125 200.068359375 c 2
190.080078125 46.703125 l 1
364.658203125 221.28515625 l 2
368.575195312 225.200195312 374.879882812 225.19921875 378.796875 221.283203125 c 2
421.23046875 178.849609375 l 2
425.146484375 174.932617188 425.147460938 168.627929688 421.232421875 164.7109375 c 2
421.23046875 164.708984375 l 1
246.650390625 -9.8671875 l 1
400.41015625 -9.8671875 l 2
405.763671875 -10.0751953125 410.015625 -14.44140625 410.015625 -19.84765625 c 2
410.015625 -80.005859375 l 2
410.015625 -85.5458984375 405.555664062 -90.005859375 400.015625 -90.005859375 c 2
EndSplineSet
Validated: 1
EndChar
StartChar: uni27A3
Encoding: 10147 10147 2
Width: 1000
VWidth: 0
Flags: HM
LayerCount: 2
Fore
SplineSet
33.16015625 636.0625 m 0
33.1669921875 636.0625 33.1728515625 636.061523438 33.1796875 636.060546875 c 0
33.619140625 636.0546875 34.056640625 636.0234375 34.4921875 635.978515625 c 0
34.6171875 635.96484375 34.8203125 635.939453125 34.9453125 635.921875 c 0
35.2578125 635.880859375 35.568359375 635.829101562 35.876953125 635.767578125 c 0
35.9912109375 635.74609375 36.1767578125 635.70703125 36.291015625 635.681640625 c 0
36.7275390625 635.583007812 37.1611328125 635.470703125 37.587890625 635.33203125 c 2
969.647460938 333.731445312 l 2
969.954101562 333.631835938 970.444335938 333.452148438 970.743164062 333.329101562 c 0
971.173828125 333.151367188 971.590820312 332.94921875 971.999023438 332.735351562 c 0
972.079101562 332.692382812 972.208007812 332.62109375 972.288085938 332.577148438 c 0
972.6015625 332.403320312 972.907226562 332.21875 973.206054688 332.024414062 c 0
973.291015625 331.969726562 973.428710938 331.879882812 973.512695312 331.823242188 c 0
973.85546875 331.587890625 974.1875 331.340820312 974.508789062 331.079101562 c 0
974.534179688 331.05859375 974.576171875 331.025390625 974.602539062 331.004882812 c 0
974.962890625 330.706054688 975.305664062 330.389648438 975.635742188 330.059570312 c 0
975.685546875 330.0078125 975.767578125 329.922851562 975.817382812 329.870117188 c 0
976.078125 329.599609375 976.329101562 329.317382812 976.569335938 329.028320312 c 0
976.62890625 328.95703125 976.725585938 328.83984375 976.784179688 328.766601562 c 0
977.045898438 328.435546875 977.295898438 328.095703125 977.530273438 327.743164062 c 0
977.545898438 327.720703125 977.571289062 327.68359375 977.586914062 327.661132812 c 0
977.844726562 327.266601562 978.081054688 326.859375 978.301757812 326.440429688 c 0
978.329101562 326.385742188 978.374023438 326.294921875 978.401367188 326.239257812 c 0
978.579101562 325.889648438 978.743164062 325.53125 978.893554688 325.166992188 c 0
978.924804688 325.090820312 978.974609375 324.965820312 979.004882812 324.887695312 c 0
979.165039062 324.475585938 979.310546875 324.056640625 979.434570312 323.627929688 c 0
979.4375 323.62109375 979.442382812 323.609375 979.4453125 323.6015625 c 0
979.446289062 323.59765625 979.447265625 323.596679688 979.448242188 323.59375 c 0
979.58203125 323.125976562 979.693359375 322.649414062 979.782226562 322.1640625 c 0
979.786132812 322.137695312 979.79296875 322.095703125 979.796875 322.069335938 c 0
979.873046875 321.640625 979.92578125 321.206054688 979.96484375 320.768554688 c 0
979.970703125 320.698242188 979.979492188 320.5859375 979.983398438 320.515625 c 0
979.99609375 320.34375 980.014648438 320.171875 980.022460938 319.998046875 c 2
980 319.998046875 l 2
980.005859375 319.84765625 980.009765625 319.602539062 980.009765625 319.451171875 c 0
980.009765625 313.670898438 975.60546875 307.362304688 970.1796875 305.369140625 c 2
38.11328125 -35.15625 l 2
36.7392578125 -35.6572265625 34.4365234375 -36.0634765625 32.9736328125 -36.0634765625 c 0
24.6923828125 -36.0634765625 17.9716796875 -29.3427734375 17.9716796875 -21.0625 c 0
17.9716796875 -19.0283203125 18.7373046875 -15.9140625 19.6796875 -14.111328125 c 2
194.083984375 319.01171875 l 1
193.5 319.998046875 l 1
193.498046875 319.998046875 l 1
20.056640625 613.4296875 l 2
19.99609375 613.537109375 19.9013671875 613.711914062 19.84375 613.8203125 c 0
19.638671875 614.190429688 19.451171875 614.564453125 19.28125 614.939453125 c 0
19.2705078125 614.962890625 19.2529296875 615.000976562 19.2421875 615.0234375 c 0
17.513671875 618.904296875 17.6025390625 623.040039062 19.037109375 626.611328125 c 0
19.046875 626.637695312 19.0634765625 626.6796875 19.07421875 626.705078125 c 0
19.244140625 627.120117188 19.431640625 627.526367188 19.63671875 627.923828125 c 0
19.67578125 628 19.740234375 628.12109375 19.78125 628.1953125 c 0
19.97265625 628.549804688 20.177734375 628.896484375 20.396484375 629.234375 c 0
20.4169921875 629.267578125 20.4501953125 629.319335938 20.470703125 629.3515625 c 0
20.7236328125 629.735351562 20.998046875 630.104492188 21.28515625 630.462890625 c 0
21.314453125 630.5 21.3623046875 630.559570312 21.392578125 630.595703125 c 0
23.8017578125 633.541992188 27.3330078125 635.6015625 31.525390625 635.98828125 c 0
31.595703125 635.99609375 31.708984375 636.005859375 31.779296875 636.01171875 c 0
32.2333984375 636.045898438 32.69140625 636.069335938 33.16015625 636.0625 c 0
226.22265625 319.998046875 m 2
226.228515625 319.842773438 226.233398438 319.58984375 226.233398438 319.43359375 c 0
226.233398438 317.405273438 225.47265625 314.298828125 224.53515625 312.5 c 2
64.236328125 6.322265625 l 1
918.96484375 318.59765625 l 1
914.63671875 319.998046875 l 1
226.22265625 319.998046875 l 2
EndSplineSet
Validated: 1
EndChar
StartChar: uni21BB
Encoding: 8635 8635 3
Width: 1000
VWidth: 0
HStem: -11.9004 80.0049<410.621 596.596> 531.894 80.0068<403.658 587.978>
VStem: 188.142 80.0029<203.446 389.421>
LayerCount: 2
Fore
SplineSet
487.42578125 611.645507812 m 0
490.903320312 611.786132812 496.55078125 611.900390625 500.032226562 611.900390625 c 0
600.807617188 611.900390625 722.643554688 540.5859375 771.986328125 452.717773438 c 1
771.994140625 452.721679688 l 1
813.62109375 600.739257812 l 2
815.270507812 605.836914062 820.623046875 608.74609375 825.828125 607.282226562 c 2
883.740234375 590.997070312 l 2
889.073242188 589.497070312 892.16015625 583.99609375 890.66015625 578.663085938 c 2
814.83203125 309.032226562 l 2
813.36328125 303.809570312 808.059570312 300.752929688 802.83203125 302.036132812 c 2
532.88671875 377.952148438 l 2
527.553710938 379.451171875 524.46875 384.951171875 525.96875 390.284179688 c 2
542.248046875 448.178710938 l 2
543.748046875 453.51171875 549.249023438 456.595703125 554.58203125 455.096679688 c 2
702.212890625 413.579101562 l 1
665.522460938 478.888671875 574.947265625 531.893554688 500.036132812 531.893554688 c 0
466.584960938 531.893554688 415.756835938 518.611328125 386.58203125 502.245117188 c 0
321.205078125 465.568359375 268.14453125 374.961914062 268.14453125 300 c 0
268.14453125 266.545898438 281.428710938 215.715820312 297.796875 186.540039062 c 0
334.473632812 121.163085938 425.079101562 68.1044921875 500.041015625 68.1044921875 c 0
533.495117188 68.1044921875 584.325195312 81.388671875 613.501953125 97.7568359375 c 1
652.64453125 27.9814453125 l 1
613.403320312 5.966796875 545.037109375 -11.900390625 500.041992188 -11.900390625 c 0
399.217773438 -11.900390625 277.353515625 59.46484375 228.0234375 147.397460938 c 0
206.008789062 186.638671875 188.141601562 255.004882812 188.141601562 300 c 0
188.141601562 400.823242188 259.505859375 522.6875 347.4375 572.018554688 c 0
383.451171875 592.221679688 446.166015625 609.974609375 487.42578125 611.645507812 c 0
EndSplineSet
Validated: 1
EndChar
StartChar: u1F50A
Encoding: 128266 128266 4
Width: 1000
VWidth: 0
HStem: 140.6 334.824<19.9492 262.816>
VStem: 19.9492 480.234<140.6 475.424> 516.504 97.4043<145.53 207.841 392.475 467.508> 538.93 104.705<166.83 432.826> 707.016 104.705<166.604 433.179> 875.107 104.535<168.541 431.014>
LayerCount: 2
Fore
SplineSet
484.7421875 634.7890625 m 0xcc
493.1953125 635.038085938 500.182617188 628.251953125 500.18359375 619.794921875 c 2
500.18359375 -19.80078125 l 2
500.176757812 -32.408203125 485.56640625 -39.3857421875 475.7578125 -31.46484375 c 2
262.81640625 140.599609375 l 1
34.94921875 140.599609375 l 2
26.6650390625 140.600585938 19.9501953125 147.315429688 19.94921875 155.599609375 c 2
19.94921875 460.423828125 l 2
19.9501953125 468.708007812 26.6650390625 475.422851562 34.94921875 475.423828125 c 2
282.64453125 475.423828125 l 1
475.7578125 631.466796875 l 2
478.30859375 633.526367188 481.46484375 634.693359375 484.7421875 634.7890625 c 0xcc
876.865234375 600.306640625 m 0
881.969726562 600.436523438 886.790039062 597.962890625 889.658203125 593.73828125 c 0
944.186523438 513.37109375 977.028320312 417.055664062 979.642578125 313.46875 c 0
979.64453125 313.34375 979.64453125 313.219726562 979.642578125 313.09375 c 2
979.642578125 287.02734375 l 2
979.64453125 286.90234375 979.64453125 286.776367188 979.642578125 286.650390625 c 0
977.047851562 183.834960938 944.668945312 88.185546875 890.869140625 8.18359375 c 0
886.895507812 2.27734375 879.317382812 -0.0224609375 872.73046875 2.677734375 c 2
802.31640625 31.578125 l 2
793.340820312 35.265625 790.171875 46.375 795.85546875 54.240234375 c 0
845.7421875 123.275390625 875.107421875 208.067382812 875.107421875 300.056640625 c 0
875.107421875 393.627929688 844.71875 479.7578125 793.2578125 549.423828125 c 0
787.33984375 557.43359375 790.725585938 568.879882812 800.048828125 572.380859375 c 2
871.978515625 599.36328125 l 2
873.54296875 599.948242188 875.1953125 600.265625 876.865234375 600.306640625 c 0
739.732421875 548.775390625 m 0
745.377929688 548.819335938 750.568359375 545.688476562 753.166015625 540.67578125 c 0
790.561523438 468.57421875 811.720703125 386.693359375 811.720703125 300.056640625 c 0
811.720703125 215.825195312 791.721679688 136.088867188 756.244140625 65.466796875 c 0
752.723632812 58.4560546875 744.397460938 55.3427734375 737.140625 58.322265625 c 2
668.083984375 86.66796875 l 2
659.944335938 90.009765625 656.415039062 99.607421875 660.451171875 107.42578125 c 0
690.209960938 165.073242188 707.015625 230.493164062 707.015625 300.056640625 c 0
707.015625 372.688476562 688.69921875 440.814453125 656.44140625 500.275390625 c 0
652.09765625 508.279296875 655.829101562 518.276367188 664.35546875 521.4765625 c 2
734.580078125 547.814453125 l 2
736.227539062 548.434570312 737.971679688 548.760742188 739.732421875 548.775390625 c 0
596.021484375 494.76953125 m 0
602.208007812 494.712890625 607.724609375 490.862304688 609.9140625 485.076171875 c 0
631.693359375 427.509765625 643.634765625 365.119140625 643.634765625 300.056640625 c 0xdc
643.634765625 239.07421875 633.157226562 180.442382812 613.908203125 125.92578125 c 0
611.05078125 117.8359375 602.000976562 113.786132812 594.064453125 117.044921875 c 2
525.0078125 145.384765625 l 2
517.669921875 148.395507812 513.944335938 156.596679688 516.50390625 164.103515625 c 0xec
531.033203125 206.7109375 538.9296875 252.407226562 538.9296875 300.056640625 c 0xdc
538.9296875 352.326171875 529.440429688 402.254882812 512.103515625 448.314453125 c 0
509.180664062 456.075195312 513.108398438 464.735351562 520.873046875 467.6484375 c 2
590.6171875 493.80859375 l 2
592.34375 494.458007812 594.176757812 494.784179688 596.021484375 494.76953125 c 0
EndSplineSet
Validated: 1
EndChar
StartChar: u1F509
Encoding: 128265 128265 5
Width: 1000
VWidth: 0
HStem: 140.6 334.824<19.9492 262.816>
VStem: 19.9492 480.234<140.6 475.424> 516.504 97.4043<145.53 207.841 392.475 467.508> 538.93 104.705<166.83 432.826>
LayerCount: 2
Fore
SplineSet
484.7421875 634.7890625 m 0xc0
493.1953125 635.038085938 500.182617188 628.251953125 500.18359375 619.794921875 c 2
500.18359375 -19.80078125 l 2
500.176757812 -32.408203125 485.56640625 -39.3857421875 475.7578125 -31.46484375 c 2
262.81640625 140.599609375 l 1
34.94921875 140.599609375 l 2
26.6650390625 140.600585938 19.9501953125 147.315429688 19.94921875 155.599609375 c 2
19.94921875 460.423828125 l 2
19.9501953125 468.708007812 26.6650390625 475.422851562 34.94921875 475.423828125 c 2
282.64453125 475.423828125 l 1
475.7578125 631.466796875 l 2
478.30859375 633.526367188 481.46484375 634.693359375 484.7421875 634.7890625 c 0xc0
596.021484375 494.76953125 m 0
602.208007812 494.712890625 607.724609375 490.862304688 609.9140625 485.076171875 c 0
631.693359375 427.509765625 643.634765625 365.119140625 643.634765625 300.056640625 c 0xd0
643.634765625 239.07421875 633.157226562 180.442382812 613.908203125 125.92578125 c 0
611.05078125 117.8359375 602.000976562 113.786132812 594.064453125 117.044921875 c 2
525.0078125 145.384765625 l 2
517.669921875 148.395507812 513.944335938 156.596679688 516.50390625 164.103515625 c 0xe0
531.033203125 206.7109375 538.9296875 252.407226562 538.9296875 300.056640625 c 0xd0
538.9296875 352.326171875 529.440429688 402.254882812 512.103515625 448.314453125 c 0
509.180664062 456.075195312 513.108398438 464.735351562 520.873046875 467.6484375 c 2
590.6171875 493.80859375 l 2
592.34375 494.458007812 594.176757812 494.784179688 596.021484375 494.76953125 c 0
EndSplineSet
Validated: 1
EndChar
StartChar: u1F507
Encoding: 128263 128263 6
Width: 1000
VWidth: 0
HStem: 140.6 334.824<19.9492 262.816>
VStem: 19.9492 480.234<140.6 475.424>
LayerCount: 2
Fore
SplineSet
484.7421875 634.7890625 m 0
493.1953125 635.038085938 500.182617188 628.251953125 500.18359375 619.794921875 c 2
500.18359375 -19.80078125 l 2
500.176757812 -32.408203125 485.56640625 -39.3857421875 475.7578125 -31.46484375 c 2
262.81640625 140.599609375 l 1
34.94921875 140.599609375 l 2
26.6650390625 140.600585938 19.9501953125 147.315429688 19.94921875 155.599609375 c 2
19.94921875 460.423828125 l 2
19.9501953125 468.708007812 26.6650390625 475.422851562 34.94921875 475.423828125 c 2
282.64453125 475.423828125 l 1
475.7578125 631.466796875 l 2
478.30859375 633.526367188 481.46484375 634.693359375 484.7421875 634.7890625 c 0
662.77734375 483.349609375 m 0
666.83203125 483.409179688 670.739257812 481.825195312 673.607421875 478.95703125 c 2
796.154296875 356.41015625 l 1
918.69921875 478.95703125 l 2
924.557617188 484.815429688 934.057617188 484.815429688 939.916015625 478.95703125 c 2
975.11328125 443.759765625 l 2
980.971679688 437.901367188 980.971679688 428.403320312 975.11328125 422.544921875 c 2
852.572265625 299.998046875 l 1
975.11328125 177.451171875 l 2
980.96875 171.595703125 980.970703125 162.1015625 975.119140625 156.2421875 c 2
939.916015625 121.0390625 l 2
934.059570312 115.178710938 924.559570312 115.174804688 918.69921875 121.03125 c 2
796.154296875 243.583984375 l 1
673.607421875 121.03125 l 2
667.747070312 115.174804688 658.247070312 115.178710938 652.390625 121.0390625 c 2
617.1875 156.2421875 l 2
611.3359375 162.1015625 611.33984375 171.595703125 617.1953125 177.451171875 c 2
739.734375 299.998046875 l 1
617.1953125 422.544921875 l 2
611.336914062 428.403320312 611.336914062 437.901367188 617.1953125 443.759765625 c 2
652.390625 478.95703125 l 2
655.149414062 481.715820312 658.875976562 483.291992188 662.77734375 483.349609375 c 0
EndSplineSet
Validated: 1
EndChar
StartChar: uni23ED
Encoding: 9197 9197 7
Width: 1000
VWidth: 0
VStem: 819.777 160.131<-34.8008 280.725 319.277 634.795>
LayerCount: 2
Fore
SplineSet
964.908203125 634.794921875 m 2
973.192382812 634.793945312 979.907226562 628.079101562 979.908203125 619.794921875 c 2
979.908203125 -19.80078125 l 2
979.907226562 -28.0849609375 973.192382812 -34.7998046875 964.908203125 -34.80078125 c 2
834.77734375 -34.80078125 l 2
826.493164062 -34.7998046875 819.778320312 -28.0849609375 819.77734375 -19.80078125 c 2
819.77734375 280.724609375 l 1
444.379882812 -31.46484375 l 2
434.571289062 -39.3857421875 419.958007812 -32.408203125 419.952148438 -19.80078125 c 2
419.952148438 280.740234375 l 1
44.533203125 -31.46484375 l 2
34.724609375 -39.3857421875 20.1142578125 -32.408203125 20.107421875 -19.80078125 c 2
20.107421875 619.794921875 l 2
20.1083984375 628.251953125 27.095703125 635.038085938 35.548828125 634.7890625 c 0
38.826171875 634.693359375 41.982421875 633.526367188 44.533203125 631.466796875 c 2
419.952148438 319.26171875 l 1
419.952148438 619.794921875 l 2
419.953125 628.251953125 426.94140625 635.038085938 435.395507812 634.7890625 c 0
438.672851562 634.693359375 441.828125 633.526367188 444.379882812 631.466796875 c 2
819.77734375 319.27734375 l 1
819.77734375 619.794921875 l 2
819.778320312 628.079101562 826.493164062 634.793945312 834.77734375 634.794921875 c 2
964.908203125 634.794921875 l 2
EndSplineSet
Validated: 1
EndChar
StartChar: uni23EE
Encoding: 9198 9198 8
Width: 1000
VWidth: 0
VStem: 20.1074 160.131<-34.8008 280.725 319.277 634.795>
LayerCount: 2
Fore
SplineSet
35.107421875 634.794921875 m 2
165.23828125 634.794921875 l 2
173.522460938 634.793945312 180.237304688 628.079101562 180.23828125 619.794921875 c 2
180.23828125 319.27734375 l 1
555.635742188 631.466796875 l 2
558.186523438 633.526367188 561.342773438 634.693359375 564.620117188 634.7890625 c 0
573.073242188 635.038085938 580.0625 628.251953125 580.063476562 619.794921875 c 2
580.063476562 319.26171875 l 1
955.482421875 631.466796875 l 2
958.033203125 633.526367188 961.189453125 634.693359375 964.466796875 634.7890625 c 0
972.919921875 635.038085938 979.907226562 628.251953125 979.908203125 619.794921875 c 2
979.908203125 -19.80078125 l 2
979.901367188 -32.408203125 965.291015625 -39.3857421875 955.482421875 -31.46484375 c 2
580.063476562 280.740234375 l 1
580.063476562 -19.80078125 l 2
580.057617188 -32.408203125 565.444335938 -39.3857421875 555.635742188 -31.46484375 c 2
180.23828125 280.724609375 l 1
180.23828125 -19.80078125 l 2
180.237304688 -28.0849609375 173.522460938 -34.7998046875 165.23828125 -34.80078125 c 2
35.107421875 -34.80078125 l 2
26.8232421875 -34.7998046875 20.1083984375 -28.0849609375 20.107421875 -19.80078125 c 2
20.107421875 619.794921875 l 2
20.1083984375 628.079101562 26.8232421875 634.793945312 35.107421875 634.794921875 c 2
EndSplineSet
Validated: 1
EndChar
StartChar: uni23EF
Encoding: 9199 9199 9
Width: 1000
VWidth: 0
VStem: 547.777 160.131<-34.8008 634.795> 819.777 160.131<-34.8008 634.795>
LayerCount: 2
Fore
SplineSet
34.798828125 634.794921875 m 0
38.3349609375 634.868164062 41.783203125 633.689453125 44.53515625 631.466796875 c 2
429.349609375 311.66796875 l 2
436.778320312 305.6640625 436.778320312 294.337890625 429.349609375 288.333984375 c 2
44.53515625 -31.46484375 l 2
34.7265625 -39.3857421875 20.1142578125 -32.408203125 20.107421875 -19.80078125 c 2
20.107421875 619.794921875 l 2
20.1064453125 627.958984375 26.6357421875 634.625 34.798828125 634.794921875 c 0
562.77734375 634.794921875 m 2
692.908203125 634.794921875 l 2
701.192382812 634.793945312 707.907226562 628.079101562 707.908203125 619.794921875 c 2
707.908203125 -19.80078125 l 2
707.907226562 -28.0849609375 701.192382812 -34.7998046875 692.908203125 -34.80078125 c 2
562.77734375 -34.80078125 l 2
554.493164062 -34.7998046875 547.778320312 -28.0849609375 547.77734375 -19.80078125 c 2
547.77734375 619.794921875 l 2
547.778320312 628.079101562 554.493164062 634.793945312 562.77734375 634.794921875 c 2
834.77734375 634.794921875 m 2
964.908203125 634.794921875 l 2
973.192382812 634.793945312 979.907226562 628.079101562 979.908203125 619.794921875 c 2
979.908203125 -19.80078125 l 2
979.907226562 -28.0849609375 973.192382812 -34.7998046875 964.908203125 -34.80078125 c 2
834.77734375 -34.80078125 l 2
826.493164062 -34.7998046875 819.778320312 -28.0849609375 819.77734375 -19.80078125 c 2
819.77734375 619.794921875 l 2
819.778320312 628.079101562 826.493164062 634.793945312 834.77734375 634.794921875 c 2
EndSplineSet
Validated: 1
EndChar
StartChar: uni2630
Encoding: 9776 9776 10
Width: 1000
VWidth: 0
HStem: -33.625 80.1309<165.202 834.798> 259.935 80.1309<165.202 834.798> 553.494 80.1309<165.202 834.798>
CounterMasks: 1 e0
LayerCount: 2
Fore
SplineSet
834.797851562 325.065429688 m 2
834.797851562 274.934570312 l 2
834.796875 266.650390625 828.08203125 259.935546875 819.797851562 259.934570312 c 2
180.202148438 259.934570312 l 2
171.91796875 259.935546875 165.203125 266.650390625 165.202148438 274.934570312 c 2
165.202148438 325.065429688 l 2
165.203125 333.349609375 171.91796875 340.064453125 180.202148438 340.065429688 c 2
819.797851562 340.065429688 l 2
828.08203125 340.064453125 834.796875 333.349609375 834.797851562 325.065429688 c 2
834.797851562 31.505859375 m 2
834.797851562 -18.625 l 2
834.796875 -26.9091796875 828.08203125 -33.6240234375 819.797851562 -33.625 c 2
180.202148438 -33.625 l 2
171.91796875 -33.6240234375 165.203125 -26.9091796875 165.202148438 -18.625 c 2
165.202148438 31.505859375 l 2
165.203125 39.7900390625 171.91796875 46.5048828125 180.202148438 46.505859375 c 2
819.797851562 46.505859375 l 2
828.08203125 46.5048828125 834.796875 39.7900390625 834.797851562 31.505859375 c 2
834.797851562 618.625 m 2
834.797851562 568.494140625 l 2
834.796875 560.209960938 828.08203125 553.495117188 819.797851562 553.494140625 c 2
180.202148438 553.494140625 l 2
171.91796875 553.495117188 165.203125 560.209960938 165.202148438 568.494140625 c 2
165.202148438 618.625 l 2
165.203125 626.909179688 171.91796875 633.624023438 180.202148438 633.625 c 2
819.797851562 633.625 l 2
828.08203125 633.624023438 834.796875 626.909179688 834.797851562 618.625 c 2
EndSplineSet
Validated: 1
EndChar
StartChar: uni2B60
Encoding: 11104 11104 11
Width: 1000
HStem: 219.932 160.131<434.739 979.995>
LayerCount: 2
Fore
SplineSet
420.047851562 634.794921875 m 0
428.2109375 634.625 434.740234375 627.958984375 434.739257812 619.794921875 c 2
434.739257812 395.0625 l 2
434.740234375 386.778320312 441.455078125 380.063476562 449.739257812 380.0625 c 2
964.995117188 380.0625 l 2
973.279296875 380.061523438 979.994140625 373.346679688 979.995117188 365.0625 c 2
979.995117188 234.931640625 l 2
979.994140625 226.647460938 973.279296875 219.932617188 964.995117188 219.931640625 c 2
449.739257812 219.931640625 l 2
441.455078125 219.930664062 434.740234375 213.215820312 434.739257812 204.931640625 c 2
434.739257812 -19.80078125 l 2
434.731445312 -32.408203125 420.120117188 -39.3857421875 410.311523438 -31.46484375 c 2
25.4970703125 288.333984375 l 2
18.068359375 294.337890625 18.068359375 305.6640625 25.4970703125 311.66796875 c 2
410.311523438 631.466796875 l 2
413.063476562 633.689453125 416.51171875 634.868164062 420.047851562 634.794921875 c 0
EndSplineSet
EndChar
StartChar: uni2B62
Encoding: 11106 11106 12
Width: 1000
HStem: 219.932 160.131<19.9258 565.182>
LayerCount: 2
Fore
SplineSet
579.873046875 634.794921875 m 0
583.409179688 634.868164062 586.857421875 633.689453125 589.609375 631.466796875 c 2
974.423828125 311.66796875 l 2
981.852539062 305.6640625 981.852539062 294.337890625 974.423828125 288.333984375 c 2
589.609375 -31.46484375 l 2
579.80078125 -39.3857421875 565.189453125 -32.408203125 565.181640625 -19.80078125 c 2
565.181640625 204.931640625 l 2
565.180664062 213.215820312 558.465820312 219.930664062 550.181640625 219.931640625 c 2
34.92578125 219.931640625 l 2
26.6416015625 219.932617188 19.9267578125 226.647460938 19.92578125 234.931640625 c 2
19.92578125 365.0625 l 2
19.9267578125 373.346679688 26.6416015625 380.061523438 34.92578125 380.0625 c 2
550.181640625 380.0625 l 2
558.465820312 380.063476562 565.180664062 386.778320312 565.181640625 395.0625 c 2
565.181640625 619.794921875 l 2
565.180664062 627.958984375 571.7109375 634.625 579.873046875 634.794921875 c 0
EndSplineSet
EndChar
StartChar: u1FA9F
Encoding: 129695 129695 13
Width: 1000
HStem: -180.002 439.97<20.001 459.968 540.034 980.001> 340.034 439.97<20.001 459.968 540.034 980.001>
VStem: 20.001 439.967<-180.002 259.968 340.034 780.004> 540.034 439.967<-180.002 259.968 340.034 780.004>
LayerCount: 2
Fore
SplineSet
546.908203125 259.967773438 m 2
973.126953125 259.967773438 l 2
976.934570312 259.967773438 980.000976562 256.90234375 980.000976562 253.09375 c 2
980.000976562 -173.127929688 l 2
980.000976562 -176.935546875 976.934570312 -180.001953125 973.126953125 -180.001953125 c 2
546.908203125 -180.001953125 l 2
543.099609375 -180.001953125 540.034179688 -176.935546875 540.034179688 -173.127929688 c 2
540.034179688 253.09375 l 2
540.034179688 256.90234375 543.099609375 259.967773438 546.908203125 259.967773438 c 2
26.8759765625 259.967773438 m 2
453.09375 259.967773438 l 2
456.901367188 259.967773438 459.967773438 256.90234375 459.967773438 253.09375 c 2
459.967773438 -173.127929688 l 2
459.967773438 -176.935546875 456.901367188 -180.001953125 453.09375 -180.001953125 c 2
26.8759765625 -180.001953125 l 2
23.0673828125 -180.001953125 20.0009765625 -176.935546875 20.0009765625 -173.127929688 c 2
20.0009765625 253.09375 l 2
20.0009765625 256.90234375 23.0673828125 259.967773438 26.8759765625 259.967773438 c 2
546.908203125 780.00390625 m 2
973.126953125 780.00390625 l 2
976.934570312 780.00390625 980.000976562 776.9375 980.000976562 773.12890625 c 2
980.000976562 346.909179688 l 2
980.000976562 343.100585938 976.934570312 340.034179688 973.126953125 340.034179688 c 2
546.908203125 340.034179688 l 2
543.099609375 340.034179688 540.034179688 343.100585938 540.034179688 346.909179688 c 2
540.034179688 773.12890625 l 2
540.034179688 776.9375 543.099609375 780.00390625 546.908203125 780.00390625 c 2
26.8759765625 780.00390625 m 2
453.09375 780.00390625 l 2
456.901367188 780.00390625 459.967773438 776.9375 459.967773438 773.12890625 c 2
459.967773438 346.909179688 l 2
459.967773438 343.100585938 456.901367188 340.034179688 453.09375 340.034179688 c 2
26.8759765625 340.034179688 l 2
23.0673828125 340.034179688 20.0009765625 343.100585938 20.0009765625 346.909179688 c 2
20.0009765625 773.12890625 l 2
20.0009765625 776.9375 23.0673828125 780.00390625 26.8759765625 780.00390625 c 2
EndSplineSet
EndChar
EndChars
EndSplineFont

View file

@ -17,10 +17,14 @@
* along with Remote-Touchpad. If not, see <http://www.gnu.org/licenses/>.
*/
package main
package inputcontrol
type PointerButton int
type Key int
import "sort"
type (
PointerButton int
Key int
)
const (
PointerButtonLeft PointerButton = iota
@ -39,34 +43,51 @@ const (
KeyBrowserBack
KeyBrowserForward
KeySuper
KeyLeft
KeyRight
KeyUp
KeyDown
KeyHome
KeyEnd
KeyBackSpace
KeyDelete
KeyReturn
KeyLimit
)
type BackendInfo struct {
type ControllerInfo struct {
Name string
Init func() (Backend, error)
Init func() (Controller, error)
priority int
}
var Backends []BackendInfo = []BackendInfo{
{"X11", InitX11Backend},
{"RemoteDesktop portal", InitPortalBackend},
{"Windows", InitWindowsBackend},
var Controllers []ControllerInfo
func RegisterController(name string, init func() (Controller, error), priority int) {
Controllers = append(Controllers, ControllerInfo{name, init, priority})
sort.SliceStable(Controllers, func(i, j int) bool {
return Controllers[i].priority < Controllers[j].priority
})
}
type UnsupportedPlatformError struct {
err error
Err error
}
func (e UnsupportedPlatformError) Error() string {
return e.err.Error()
func (e *UnsupportedPlatformError) Error() string {
return e.Err.Error()
}
type Backend interface {
func (e *UnsupportedPlatformError) Unwrap() error {
return e.Err
}
type Controller interface {
Close() error
KeyboardText(text string) error
KeyboardKey(key Key) error
PointerButton(button PointerButton, press bool) error
PointerMove(deltaX, deltaY int) error
PointerScroll(deltaHorizontal, deltaVertical int) error
PointerScrollFinish() error
PointerScroll(deltaHorizontal, deltaVertical int, finish bool) error
}

View file

@ -0,0 +1,66 @@
//go:build null
/*
* Copyright (c) 2023 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 inputcontrol
import (
"log"
)
type nullController struct{}
func init() {
RegisterController("null", InitNullController, 1000)
}
func InitNullController() (Controller, error) {
return &nullController{}, nil
}
func (p *nullController) Close() error {
return nil
}
func (p *nullController) KeyboardText(text string) error {
log.Printf("KeyboardText(text: %#v)", text)
return nil
}
func (p *nullController) KeyboardKey(key Key) error {
log.Printf("KeyboardKey(key: %#v)", key)
return nil
}
func (p *nullController) PointerButton(button PointerButton, press bool) error {
log.Printf("PointerButton(button: %#v, press: %#v)", button, press)
return nil
}
func (p *nullController) PointerMove(deltaX, deltaY int) error {
log.Printf("PointerMove(deltaX: %#v, deltaY: %#v)", deltaX, deltaY)
return nil
}
func (p *nullController) PointerScroll(deltaHorizontal, deltaVertical int, finish bool) error {
log.Printf("PointerScroll(deltaHorizontal: %#v, deltaVertical: %#v, finish: %#v)",
deltaHorizontal, deltaVertical, finish)
return nil
}

View file

@ -0,0 +1,400 @@
//go:build portal
/*
* Copyright (c) 2018 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 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"
)
const (
deviceKeyboard uint32 = 1
devicePointer uint32 = 2
btnReleased uint32 = 0
btnPressed uint32 = 1
untilRevoked uint32 = 2
// linux/input-event-codes.h
btnLeft int32 = 0x110
btnRight int32 = 0x111
btnMiddle int32 = 0x112
)
type portalController struct {
bus *dbus.Conn
portalDesktop dbus.BusObject
sessionHandle dbus.ObjectPath
}
func init() {
RegisterController("RemoteDesktop portal", InitPortalController, 1)
}
func InitPortalController() (Controller, error) {
bus, err := dbus.SessionBusPrivate()
if err != nil {
return nil, &UnsupportedPlatformError{err}
}
cleanupBus := true
defer func() {
if cleanupBus {
bus.Close()
}
}()
err = bus.Auth(nil)
if err != nil {
return nil, &UnsupportedPlatformError{err}
}
err = bus.Hello()
if err != nil {
return nil, &UnsupportedPlatformError{err}
}
portalDesktop := 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(
"org.freedesktop.portal.RemoteDesktop.AvailableDeviceTypes")
if err != nil {
return nil, &UnsupportedPlatformError{
fmt.Errorf("getting 'AvailableDeviceTypes' failed: %w", err),
}
}
availableDeviceTypes, ok := availableDeviceTypesV.Value().(uint32)
if !ok {
return nil, &UnsupportedPlatformError{
errors.New("unexpected 'AvailableDeviceTypes' return type"),
}
}
if availableDeviceTypes&deviceKeyboard == 0 ||
availableDeviceTypes&devicePointer == 0 {
return nil, &UnsupportedPlatformError{
errors.New("keyboard or pointer source type not supported"),
}
}
createSessionResults, err := checkResponse(getResponse(bus, portalDesktop,
"org.freedesktop.portal.RemoteDesktop.CreateSession", 0,
map[string]dbus.Variant{"session_handle_token": dbus.MakeVariant("t")},
))
if err != nil {
return nil, &UnsupportedPlatformError{
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"),
}
}
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 {
return nil, &UnsupportedPlatformError{
fmt.Errorf("calling 'SelectDevices' failed: %w", err),
}
}
startResponseStatus, startResults, err := getResponse(bus, portalDesktop,
"org.freedesktop.portal.RemoteDesktop.Start", 0,
sessionHandle, "", map[string]dbus.Variant{},
)
if err != nil {
return nil, &UnsupportedPlatformError{
fmt.Errorf("calling 'Start' failed: %w", err),
}
}
if startResponseStatus != 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)
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)
}
func getResponse(bus *dbus.Conn, object dbus.BusObject, method string,
flags dbus.Flags, args ...interface{},
) (uint32, map[string]dbus.Variant, error) {
ch := make(chan *dbus.Signal, 512)
bus.Signal(ch)
defer bus.RemoveSignal(ch)
var requestPath dbus.ObjectPath
if err := object.Call(method, flags, args...).Store(&requestPath); err != nil {
return 0, nil, err
}
for {
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))
}
responseStatus, 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)
if !ok {
return 0, nil, errors.New("unexpected 'Response' return type")
}
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 {
return p.bus.Close()
}
func (p *portalController) keyboardKeys(keys []Keysym) error {
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 {
return err
}
}
}
return nil
}
func (p *portalController) KeyboardText(text string) error {
keys := make([]Keysym, 0, len(text))
for _, runeValue := range text {
keysym, err := RuneToKeysym(runeValue)
if err != nil {
return err
}
keys = append(keys, keysym)
}
return p.keyboardKeys(keys)
}
func (p *portalController) KeyboardKey(key Key) error {
keysym, err := KeyToKeysym(key)
if err != nil {
return err
}
keys := [...]Keysym{keysym}
return p.keyboardKeys(keys[:])
}
func (p *portalController) PointerButton(button PointerButton, press bool) error {
var btn int32
switch button {
case PointerButtonLeft:
btn = btnLeft
case PointerButtonMiddle:
btn = btnMiddle
case PointerButtonRight:
btn = btnRight
default:
return fmt.Errorf("unsupported pointer button: %#v", button)
}
state := btnReleased
if press {
state = btnPressed
}
if err := p.portalDesktop.Call("org.freedesktop.portal.RemoteDesktop.NotifyPointerButton", 0,
p.sessionHandle, map[string]dbus.Variant{}, btn, state,
).Store(); err != nil {
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 {
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 {
return err
}
return nil
}

View file

@ -0,0 +1,200 @@
//go:build uinput
/*
* Copyright (c) 2023 De_Coder github.com/ps100000
* Copyright (c) 2023 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 inputcontrol
import (
"errors"
"fmt"
"log"
"os"
"time"
"github.com/bendahl/uinput"
)
const shiftKeysDelay time.Duration = 50 * time.Millisecond
type uinputController struct {
keymap *Keymap
keyboard uinput.Keyboard
mouse uinput.Mouse
}
func init() {
RegisterController("uinput", InitUinputController, 10)
}
func InitUinputController() (Controller, error) {
keymapName, keymapSet := os.LookupEnv("REMOTE_TOUCHPAD_UINPUT_KEYMAP")
if !keymapSet {
keymapName = "defkeymap"
}
keymap, err := LoadKeymap(keymapName)
if err != nil {
return nil, err
}
keyboard, err := uinput.CreateKeyboard("/dev/uinput", []byte("remote-touchpad-keyboard"))
if errors.Is(err, os.ErrNotExist) || errors.Is(err, os.ErrPermission) {
err = &UnsupportedPlatformError{err}
}
if err != nil {
return nil, err
}
mouse, err := uinput.CreateMouse("/dev/uinput", []byte("remote-touchpad-mouse"))
if errors.Is(err, os.ErrNotExist) || errors.Is(err, os.ErrPermission) {
err = &UnsupportedPlatformError{err}
}
if err != nil {
keyboard.Close()
return nil, err
}
if !keymapSet {
log.Print("Hint: Set the keyboard mapping with the REMOTE_TOUCHPAD_UINPUT_KEYMAP environment variable")
}
return &uinputController{keymap, keyboard, mouse}, nil
}
func (p *uinputController) Close() error {
return errors.Join(p.keyboard.Close(), p.mouse.Close())
}
func (p *uinputController) KeyboardText(text string) error {
var activeShiftKeys KeyCombo
updateShiftKeys := func(keyCombo KeyCombo) error {
if activeShiftKeys.ShiftKeys == keyCombo.ShiftKeys {
activeShiftKeys.Key = keyCombo.Key
return nil
}
if activeShiftKeys.Key != 0 {
time.Sleep(shiftKeysDelay)
activeShiftKeys.Key = 0
}
for i := range keyCombo.ShiftKeys {
if activeShiftKeys.ShiftKeys[i] != keyCombo.ShiftKeys[i] {
if activeShiftKeys.ShiftKeys[i] != 0 {
if err := p.keyboard.KeyUp(activeShiftKeys.ShiftKeys[i]); err != nil {
return err
}
activeShiftKeys.ShiftKeys[i] = 0
}
if keyCombo.ShiftKeys[i] != 0 {
if err := p.keyboard.KeyDown(keyCombo.ShiftKeys[i]); err != nil {
return err
}
activeShiftKeys.ShiftKeys[i] = keyCombo.ShiftKeys[i]
}
}
}
if keyCombo.Key != 0 {
activeShiftKeys.Key = keyCombo.Key
time.Sleep(shiftKeysDelay)
}
return nil
}
defer updateShiftKeys(KeyCombo{})
for _, runeValue := range text {
keyCombo, found := p.keymap.Get(runeValue)
if !found {
return fmt.Errorf("unsupported rune: %q", runeValue)
}
if err := updateShiftKeys(keyCombo); err != nil {
return err
}
if err := p.keyboard.KeyPress(keyCombo.Key); err != nil {
return err
}
}
return updateShiftKeys(KeyCombo{Key: 1})
}
func (p *uinputController) KeyboardKey(key Key) error {
var uinputKey int
switch key {
case KeyBackSpace:
uinputKey = uinput.KeyBackspace
case KeyReturn:
uinputKey = uinput.KeyEnter
case KeyDelete:
uinputKey = uinput.KeyDelete
case KeyHome:
uinputKey = uinput.KeyHome
case KeyLeft:
uinputKey = uinput.KeyLeft
case KeyUp:
uinputKey = uinput.KeyUp
case KeyRight:
uinputKey = uinput.KeyRight
case KeyDown:
uinputKey = uinput.KeyDown
case KeyEnd:
uinputKey = uinput.KeyEnd
case KeySuper:
uinputKey = uinput.KeyLeftmeta
case KeyVolumeMute:
uinputKey = uinput.KeyMute
case KeyVolumeDown:
uinputKey = uinput.KeyVolumedown
case KeyVolumeUp:
uinputKey = uinput.KeyVolumeup
case KeyMediaPlayPause:
uinputKey = uinput.KeyPlaypause
case KeyMediaPrevTrack:
uinputKey = uinput.KeyPrevioussong
case KeyMediaNextTrack:
uinputKey = uinput.KeyNextsong
case KeyBrowserBack:
uinputKey = uinput.KeyBack
case KeyBrowserForward:
uinputKey = uinput.KeyForward
default:
return fmt.Errorf("unsupported key: %#v", key)
}
return p.keyboard.KeyPress(uinputKey)
}
func (p *uinputController) PointerButton(button PointerButton, press bool) error {
switch {
case button == PointerButtonLeft && press:
return p.mouse.LeftPress()
case button == PointerButtonLeft && !press:
return p.mouse.LeftRelease()
case button == PointerButtonRight && press:
return p.mouse.RightPress()
case button == PointerButtonRight && !press:
return p.mouse.RightRelease()
case button == PointerButtonMiddle && press:
return p.mouse.MiddlePress()
case button == PointerButtonMiddle && !press:
return p.mouse.MiddleRelease()
default:
return fmt.Errorf("unsupported pointer button: %#v", button)
}
}
func (p *uinputController) PointerMove(deltaX, deltaY int) error {
return p.mouse.Move(int32(deltaX), int32(deltaY))
}
func (p *uinputController) PointerScroll(deltaHorizontal, deltaVertical int, finish bool) error {
return errors.Join(p.mouse.Wheel(false, int32(-deltaVertical)), p.mouse.Wheel(true, int32(deltaHorizontal)))
}

View file

@ -1,4 +1,4 @@
// +build windows
//go:build windows
/*
* Copyright (c) 2018 Unrud <unrud@outlook.com>
@ -19,10 +19,10 @@
* along with Remote-Touchpad. If not, see <http://www.gnu.org/licenses/>.
*/
package main
package inputcontrol
import (
"errors"
"fmt"
"syscall"
"unsafe"
)
@ -34,6 +34,15 @@ const (
keyeventfKeyup uint32 = 0x2
keyeventfUnicode uint32 = 0x4
vkBack uint16 = 0x8
vkReturn uint16 = 0xD
vkEnd uint16 = 0x23
vkHome uint16 = 0x24
vkLeft uint16 = 0x25
vkUp uint16 = 0x26
vkRight uint16 = 0x27
vkDown uint16 = 0x28
vkDelete uint16 = 0x2E
vkLwin uint16 = 0x5B
vkBrowserBack uint16 = 0xA6
vkBrowserForward uint16 = 0xA7
@ -80,33 +89,36 @@ type keybdInput struct {
padding [8]byte
}
type windowsBackend struct{}
type windowsController struct{}
func InitWindowsBackend() (Backend, error) {
p := &windowsBackend{}
func init() {
RegisterController("Windows", InitWindowsController, 0)
}
func InitWindowsController() (Controller, error) {
p := &windowsController{}
if err := sendInputProc.Find(); err != nil {
return nil, UnsupportedPlatformError{err}
return nil, &UnsupportedPlatformError{err}
}
return p, nil
}
func (p *windowsBackend) Close() error {
func (p *windowsController) Close() error {
return nil
}
func (p *windowsBackend) sendInput(inputs []keybdInput) error {
func (p *windowsController) sendInput(inputs []keybdInput) error {
if len(inputs) == 0 {
return nil
}
if r, _, err := sendInputProc.Call(uintptr(len(inputs)),
uintptr(unsafe.Pointer(&inputs[0])),
unsafe.Sizeof(inputs[0])); int(r) != len(inputs) {
if sent, _, err := sendInputProc.Call(uintptr(len(inputs)),
uintptr(unsafe.Pointer(&inputs[0])), unsafe.Sizeof(inputs[0])); int(sent) != len(inputs) {
return err
}
return nil
}
func (p *windowsBackend) KeyboardText(text string) error {
func (p *windowsController) KeyboardText(text string) error {
inputs := make([]keybdInput, 0, len(text)*2)
for _, runeValue := range text {
in := keybdInput{typ: inputKeyboard, wScan: uint16(runeValue), dwFlags: keyeventfUnicode}
@ -120,35 +132,54 @@ func (p *windowsBackend) KeyboardText(text string) error {
return p.sendInput(inputs)
}
func (p *windowsBackend) KeyboardKey(key Key) error {
func (p *windowsController) KeyboardKey(key Key) error {
input := keybdInput{typ: inputKeyboard}
if key == KeySuper {
switch key {
case KeyBackSpace:
input.wVk = vkBack
case KeyReturn:
input.wVk = vkReturn
case KeyEnd:
input.wVk = vkEnd
case KeyHome:
input.wVk = vkHome
case KeyLeft:
input.wVk = vkLeft
case KeyUp:
input.wVk = vkUp
case KeyRight:
input.wVk = vkRight
case KeyDown:
input.wVk = vkDown
case KeyDelete:
input.wVk = vkDelete
case KeySuper:
input.wVk = vkLwin
} else if key == KeyBrowserBack {
case KeyBrowserBack:
input.wVk = vkBrowserBack
} else if key == KeyBrowserForward {
case KeyBrowserForward:
input.wVk = vkBrowserForward
} else if key == KeyVolumeMute {
case KeyVolumeMute:
input.wVk = vkVolumeMute
} else if key == KeyVolumeDown {
case KeyVolumeDown:
input.wVk = vkVolumeDown
} else if key == KeyVolumeUp {
case KeyVolumeUp:
input.wVk = vkVolumeUp
} else if key == KeyMediaNextTrack {
case KeyMediaNextTrack:
input.wVk = vkMediaNextTrack
} else if key == KeyMediaPrevTrack {
case KeyMediaPrevTrack:
input.wVk = vkMediaPrevTrack
} else if key == KeyMediaPlayPause {
case KeyMediaPlayPause:
input.wVk = vkMediaPlayPause
} else {
return errors.New("key not mapped to virtual-key code")
default:
return fmt.Errorf("key not mapped to virtual-key code: %#v", key)
}
inputs := [...]keybdInput{input, input}
inputs[1].dwFlags |= keyeventfKeyup
return p.sendInput(inputs[:])
}
func (p *windowsBackend) PointerButton(button PointerButton, press bool) error {
func (p *windowsController) PointerButton(button PointerButton, press bool) error {
input := mouseInput{typ: inputMouse}
if button == PointerButtonLeft && press {
input.dwFlags = mouseeventfLeftdown
@ -163,30 +194,30 @@ func (p *windowsBackend) PointerButton(button PointerButton, press bool) error {
} else if button == PointerButtonRight {
input.dwFlags = mouseeventfRightup
} else {
return errors.New("unsupported pointer button")
return fmt.Errorf("unsupported pointer button: %#v", button)
}
if r, _, err := sendInputProc.Call(1, uintptr(unsafe.Pointer(&input)),
unsafe.Sizeof(input)); int(r) != 1 {
if sent, _, err := sendInputProc.Call(1, uintptr(unsafe.Pointer(&input)),
unsafe.Sizeof(input)); int(sent) != 1 {
return err
}
return nil
}
func (p *windowsBackend) PointerMove(deltaX, deltaY int) error {
func (p *windowsController) PointerMove(deltaX, deltaY int) error {
input := mouseInput{
typ: inputMouse,
dx: int32(deltaX),
dy: int32(deltaY),
dwFlags: mouseeventfMove,
}
if r, _, err := sendInputProc.Call(1, uintptr(unsafe.Pointer(&input)),
unsafe.Sizeof(input)); int(r) != 1 {
if sent, _, err := sendInputProc.Call(1, uintptr(unsafe.Pointer(&input)),
unsafe.Sizeof(input)); int(sent) != 1 {
return err
}
return nil
}
func (p *windowsBackend) PointerScroll(deltaHorizontal, deltaVertical int) error {
func (p *windowsController) PointerScroll(deltaHorizontal, deltaVertical int, finish bool) error {
inputs := make([]mouseInput, 0, 2)
if deltaHorizontal != 0 {
inputs = append(inputs, mouseInput{
@ -199,20 +230,15 @@ func (p *windowsBackend) PointerScroll(deltaHorizontal, deltaVertical int) error
inputs = append(inputs, mouseInput{
typ: inputMouse,
dwFlags: mouseeventfWheel,
mouseData: uint32(deltaVertical * scrollMult),
mouseData: uint32(-deltaVertical * scrollMult),
})
}
if len(inputs) == 0 {
return nil
}
if r, _, err := sendInputProc.Call(uintptr(len(inputs)),
uintptr(unsafe.Pointer(&inputs[0])),
unsafe.Sizeof(inputs[0])); int(r) != len(inputs) {
if sent, _, err := sendInputProc.Call(uintptr(len(inputs)),
uintptr(unsafe.Pointer(&inputs[0])), unsafe.Sizeof(inputs[0])); int(sent) != len(inputs) {
return err
}
return nil
}
func (p *windowsBackend) PointerScrollFinish() error {
return nil
}

View file

@ -1,4 +1,4 @@
// +build x11
//go:build x11
/*
* Copyright (c) 2018 Unrud <unrud@outlook.com>
@ -19,52 +19,101 @@
* along with Remote-Touchpad. If not, see <http://www.gnu.org/licenses/>.
*/
package main
package inputcontrol
// #cgo LDFLAGS: -lX11 -lXtst
// #cgo LDFLAGS: -lX11 -lXrandr -lXtst
// #include <stdlib.h>
// #include <X11/Xlib.h>
// #include <X11/Intrinsic.h>
// #include <X11/extensions/Xrandr.h>
// #include <X11/extensions/XTest.h>
// #include <X11/XKBlib.h>
// Window MacroDefaultRootWindow(Display *dpy) {
// return DefaultRootWindow(dpy);
// }
import "C"
import (
"errors"
"fmt"
"os"
"maps"
"slices"
"strings"
"sync"
"time"
"unsafe"
)
const (
keyboardMappingDelay time.Duration = 500 * time.Millisecond
keyboardMappingDelay time.Duration = 200 * time.Millisecond
scrollDiv int = 20
)
var modifierIndices [6]uint = [...]uint{C.ShiftMapIndex, C.Mod1MapIndex,
C.Mod2MapIndex, C.Mod3MapIndex, C.Mod4MapIndex, C.Mod5MapIndex}
var modifierIndexes = [...]int{
C.ShiftMapIndex,
C.Mod1MapIndex,
C.Mod2MapIndex,
C.Mod3MapIndex,
C.Mod4MapIndex,
C.Mod5MapIndex,
}
type x11Backend struct {
type x11Controller struct {
display *C.Display
lock sync.Mutex
scrollHorizontal, scrollVertical int
}
func InitX11Backend() (Backend, error) {
sessionType := os.Getenv("XDG_SESSION_TYPE")
if sessionType != "" && sessionType != "x11" {
return nil, UnsupportedPlatformError{errors.New(fmt.Sprintf(
"unsupported session type '%v'", sessionType))}
}
display := C.XOpenDisplay(nil)
if display == nil {
return nil, UnsupportedPlatformError{
errors.New("failed to connect to X server")}
}
return &x11Backend{display: display}, nil
func init() {
RegisterController("X11", InitX11Controller, 0)
}
func (p *x11Backend) Close() error {
func InitX11Controller() (Controller, error) {
display := C.XOpenDisplay(nil)
if display == nil {
return nil, &UnsupportedPlatformError{
errors.New("failed to connect to X server"),
}
}
p := &x11Controller{display: display}
if p.xIsXwayland() {
p.Close()
return nil, &UnsupportedPlatformError{
errors.New("X server is Xwayland"),
}
}
return p, nil
}
func (p *x11Controller) xIsXwayland() bool {
// Detection method from https://gitlab.freedesktop.org/xorg/app/xisxwayland/-/blob/xisxwayland-2/xisxwayland.c
var opcode, event, error, major, minor C.int
xwaylandExtensionName := C.CString("XWAYLAND")
defer C.free(unsafe.Pointer(xwaylandExtensionName))
if C.XQueryExtension(p.display, xwaylandExtensionName, &opcode, &event, &error) != 0 {
return true
}
if C.XRRQueryExtension(p.display, &event, &error) == 0 ||
C.XRRQueryVersion(p.display, &major, &minor) == 0 {
return false
}
resources := C.XRRGetScreenResourcesCurrent(p.display, C.MacroDefaultRootWindow(p.display))
if resources == nil {
return false
}
defer C.XRRFreeScreenResources(resources)
if resources.noutput < 1 {
return false
}
output := C.XRRGetOutputInfo(p.display, resources, *resources.outputs)
if output == nil {
return false
}
defer C.XRRFreeOutputInfo(output)
return strings.HasPrefix(C.GoString(output.name), "XWAYLAND")
}
func (p *x11Controller) Close() error {
p.lock.Lock()
defer p.lock.Unlock()
if p.display == nil {
@ -75,53 +124,68 @@ func (p *x11Backend) Close() error {
return nil
}
func (p *x11Backend) 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 *x11Backend) 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 *x11Backend) getModKeycodesLocked() map[uint]C.KeyCode {
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<<uint(modIndex)] = keycode
modKeycodes[1<<modIndex] = keycode
break
}
}
@ -129,56 +193,45 @@ func (p *x11Backend) getModKeycodesLocked() map[uint]C.KeyCode {
return modKeycodes
}
func (p *x11Backend) findKeycodeLocked(keyboard C.XkbDescPtr,
modKeycodes map[uint]C.KeyCode, activeMods C.uint,
keysym Keysym) (C.KeyCode, C.uint) {
keycode := C.XKeysymToKeycode(p.display, C.KeySym(keysym))
func (p *x11Controller) findKeycodeLocked(
keyboard C.XkbDescPtr, modKeycodes map[uint]C.KeyCode, activeMods uint, keysym Keysym,
) (keycode C.KeyCode, mods uint) {
keycode = C.XKeysymToKeycode(p.display, C.KeySym(keysym))
if keycode == 0 {
return 0, 0
}
var alwaysActiveMods C.uint
for modIndex := uint(0); modIndex < 8; modIndex++ {
mod := uint(1) << modIndex
if _, modAvailable := modKeycodes[mod]; !modAvailable {
alwaysActiveMods |= activeMods & C.uint(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
for activeModBitmap := range uint(1) << len(modKeycodes) {
mods := activeMods
for i, mod := range slices.Sorted(maps.Keys(modKeycodes)) {
if activeModBitmap&(1<<i) != 0 {
mods ^= mod
}
}
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
}
func (p *x11Backend) sendModsLocked(modKeycodes map[uint]C.KeyCode, mods C.uint,
press bool) {
func (p *x11Controller) sendModsLocked(
modKeycodes map[uint]C.KeyCode, mods uint, press bool,
) {
var pressC C.int = C.False
if press {
pressC = C.True
}
for mod, keycode := range modKeycodes {
if mods&C.uint(mod) != 0 {
if mods&mod != 0 {
C.XTestFakeKeyEvent(p.display, C.uint(keycode), pressC, 0)
}
}
}
func (p *x11Backend) keyboardKeys(keys []Keysym) error {
func (p *x11Controller) keyboardKeys(keys []Keysym) error {
p.lock.Lock()
defer p.lock.Unlock()
if p.display == nil {
@ -193,17 +246,22 @@ func (p *x11Backend) keyboardKeys(keys []Keysym) error {
C.XkbCompatMapMask|C.XkbGeometryMask, C.XkbUseCoreKbd)
defer C.XkbFreeKeyboard(keyboard, C.XkbAllComponentsMask, C.True)
var emptyKeycode C.KeyCode
var keysymsPerKeycode C.int
var keysymsPerKeycode int
for _, keysym := range keys {
var root, child C.Window
var rootX, rootY, x, y C.int
var activeMods C.uint
C.XSync(p.display, C.False)
C.XQueryPointer(p.display, rootWindow, &root, &child, &rootX, &rootY,
&x, &y, &activeMods)
keycode, mods := p.findKeycodeLocked(keyboard, modKeycodes, activeMods,
keysym)
var pressMods, releaseMods C.uint
var activeModsC C.uint
C.XQueryPointer(
p.display,
rootWindow,
&root, &child,
&rootX, &rootY,
&x, &y,
&activeModsC,
)
activeMods := uint(activeModsC)
keycode, mods := p.findKeycodeLocked(keyboard, modKeycodes, activeMods, keysym)
var pressMods, releaseMods uint
if keycode == 0 {
if emptyKeycode == 0 {
var err error
@ -211,12 +269,10 @@ func (p *x11Backend) keyboardKeys(keys []Keysym) error {
if err != nil {
return err
}
defer p.changeKeyMappingLocked(keysymsPerKeycode, emptyKeycode, 0)
defer p.changeKeyMappingLocked(emptyKeycode, keysymsPerKeycode, 0)
}
keycode = emptyKeycode
p.changeKeyMappingLocked(keysymsPerKeycode, keycode, keysym)
// race condition!
time.Sleep(keyboardMappingDelay)
p.changeKeyMappingLocked(keycode, keysymsPerKeycode, keysym)
} else {
pressMods = mods & ^activeMods
releaseMods = activeMods & ^mods
@ -227,7 +283,7 @@ func (p *x11Backend) keyboardKeys(keys []Keysym) error {
C.XTestFakeKeyEvent(p.display, C.uint(keycode), C.False, 0)
p.sendModsLocked(modKeycodes, pressMods, false)
p.sendModsLocked(modKeycodes, releaseMods, true)
C.XFlush(p.display)
C.XSync(p.display, C.False)
if keycode == emptyKeycode {
// race condition!
time.Sleep(keyboardMappingDelay)
@ -236,7 +292,7 @@ func (p *x11Backend) keyboardKeys(keys []Keysym) error {
return nil
}
func (p *x11Backend) KeyboardText(text string) error {
func (p *x11Controller) KeyboardText(text string) error {
keys := make([]Keysym, 0, len(text))
for _, runeValue := range text {
keysym, err := RuneToKeysym(runeValue)
@ -248,7 +304,7 @@ func (p *x11Backend) KeyboardText(text string) error {
return p.keyboardKeys(keys)
}
func (p *x11Backend) KeyboardKey(key Key) error {
func (p *x11Controller) KeyboardKey(key Key) error {
keysym, err := KeyToKeysym(key)
if err != nil {
return err
@ -257,7 +313,7 @@ func (p *x11Backend) KeyboardKey(key Key) error {
return p.keyboardKeys(keys[:])
}
func (p *x11Backend) sendButton(button uint, press bool) error {
func (p *x11Controller) sendButton(button uint, press bool) error {
p.lock.Lock()
defer p.lock.Unlock()
if p.display == nil {
@ -271,47 +327,52 @@ func (p *x11Backend) sendButton(button uint, press bool) error {
pressC = C.True
}
C.XTestFakeButtonEvent(p.display, C.uint(button), pressC, 0)
C.XFlush(p.display)
C.XSync(p.display, C.False)
return nil
}
func (p *x11Backend) PointerButton(button PointerButton, press bool) error {
if button == PointerButtonLeft {
func (p *x11Controller) PointerButton(button PointerButton, press bool) error {
switch button {
case PointerButtonLeft:
return p.sendButton(1, press)
}
if button == PointerButtonRight {
case PointerButtonRight:
return p.sendButton(3, press)
}
if button == PointerButtonMiddle {
case PointerButtonMiddle:
return p.sendButton(2, press)
default:
return fmt.Errorf("unsupported pointer button: %#v", button)
}
return errors.New("unsupported pointer button")
}
func (p *x11Backend) PointerMove(deltaX, deltaY int) error {
func (p *x11Controller) PointerMove(deltaX, deltaY int) error {
p.lock.Lock()
defer p.lock.Unlock()
if p.display == nil {
return errors.New("X server connection closed")
}
C.XTestFakeRelativeMotionEvent(p.display, C.int(deltaX), C.int(deltaY), 0)
C.XFlush(p.display)
C.XSync(p.display, C.False)
return nil
}
func (p *x11Backend) PointerScroll(deltaHorizontal, deltaVertical int) error {
func (p *x11Controller) PointerScroll(deltaHorizontal, deltaVertical int, finish bool) error {
p.lock.Lock()
stepsHorizontal := (p.scrollHorizontal + deltaHorizontal) / scrollDiv
stepsVertical := (p.scrollVertical + deltaVertical) / scrollDiv
p.scrollHorizontal = (p.scrollHorizontal + deltaHorizontal) % scrollDiv
p.scrollVertical = (p.scrollVertical + deltaVertical) % scrollDiv
if finish {
p.scrollHorizontal = 0
p.scrollVertical = 0
} else {
p.scrollHorizontal = (p.scrollHorizontal + deltaHorizontal) % scrollDiv
p.scrollVertical = (p.scrollVertical + deltaVertical) % scrollDiv
}
p.lock.Unlock()
var buttonHorizontal uint = 7
if stepsHorizontal < 0 {
buttonHorizontal = 6
stepsHorizontal = -stepsHorizontal
}
for i := 0; i < stepsHorizontal; i++ {
for range stepsHorizontal {
if err := p.sendButton(buttonHorizontal, true); err != nil {
return err
}
@ -324,7 +385,7 @@ func (p *x11Backend) PointerScroll(deltaHorizontal, deltaVertical int) error {
buttonVertical = 4
stepsVertical = -stepsVertical
}
for i := 0; i < stepsVertical; i++ {
for range stepsVertical {
if err := p.sendButton(buttonVertical, true); err != nil {
return err
}
@ -334,11 +395,3 @@ func (p *x11Backend) PointerScroll(deltaHorizontal, deltaVertical int) error {
}
return nil
}
func (p *x11Backend) PointerScrollFinish() error {
p.lock.Lock()
defer p.lock.Unlock()
p.scrollHorizontal = 0
p.scrollVertical = 0
return nil
}

130
inputcontrol/keymap.go Normal file
View file

@ -0,0 +1,130 @@
//go:build uinput
/*
* Copyright (c) 2023 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 inputcontrol
import (
"fmt"
"os/exec"
"strings"
"unsafe"
)
const bkeymapFileSignature = "bkeymap"
// uapi/linux/keyboard.h
const (
nrKeys = 128
maxNrKeymaps = 256
nrShift = 9
ktLatin = 0
ktShift = 7
ktLetter = 11
)
type KeyCombo struct {
Key int
ShiftKeys [nrShift]int
}
type Keymap struct {
keyComboMap map[rune]KeyCombo
}
func (keymap Keymap) Get(character rune) (keyCombo KeyCombo, found bool) {
keyCombo, found = keymap.keyComboMap[character]
return
}
func LoadKeymap(name string) (*Keymap, error) {
if len(name) == 0 || strings.HasPrefix(name, "-") {
return nil, fmt.Errorf("invalid keymap name: %#v", name)
}
bkeymap, err := exec.Command("loadkeys", "--bkeymap", name).Output()
if err != nil {
return nil, &UnsupportedPlatformError{err}
}
if !strings.HasPrefix(string(bkeymap), bkeymapFileSignature) {
return nil, fmt.Errorf("invalid bkeymap: signature not found")
}
bkeymapHeader := bkeymap[len(bkeymapFileSignature):]
var keymaps [][nrKeys]int
if len(bkeymapHeader) < maxNrKeymaps {
return nil, fmt.Errorf("invalid bkeymap: EOF")
}
currentBkeymap := bkeymapHeader[maxNrKeymaps:]
for keymapIndex := 0; keymapIndex < maxNrKeymaps; keymapIndex += 1 {
if bkeymapHeader[keymapIndex] == 0 {
continue
}
if len(currentBkeymap) < nrKeys*2 {
return nil, fmt.Errorf("invalid bkeymap: EOF")
}
for len(keymaps) <= keymapIndex {
keymaps = append(keymaps, [nrKeys]int{})
}
for key := 0; key < nrKeys; key += 1 {
var keysym uint16
copy(unsafe.Slice((*byte)(unsafe.Pointer(&keysym)), 2),
currentBkeymap[key*2:])
keymaps[keymapIndex][key] = int(keysym)
}
currentBkeymap = currentBkeymap[nrKeys*2:]
}
keymap := Keymap{keyComboMap: make(map[rune]KeyCombo)}
if len(keymaps) == 0 {
return &keymap, nil
}
var shiftKeys [nrShift]int
for key, keysym := range keymaps[0] {
keyType := keysym >> 8
keyValue := keysym & 0xff
if keyType == ktShift && keyValue < nrShift && shiftKeys[keyValue] == 0 {
shiftKeys[keyValue] = key
}
}
keymapLoop:
for keymapIndex := range keymaps {
var activeShiftKeys [nrShift]int
for i, shiftKey := range shiftKeys {
if keymapIndex&(1<<i) != 0 {
if shiftKey == 0 {
continue keymapLoop
}
activeShiftKeys[i] = shiftKey
}
}
for key, keysym := range keymaps[keymapIndex] {
keyType := keysym >> 8
keyValue := keysym & 0xff
if (keyType != ktLatin && keyType != ktLetter) || keyValue == 0 {
continue
}
character := rune(keyValue)
if _, ok := keymap.keyComboMap[character]; ok {
continue
}
keymap.keyComboMap[character] = KeyCombo{key, activeShiftKeys}
}
}
return &keymap, nil
}

View file

@ -1,7 +1,7 @@
// Code generated by keysyms.generator.go. DO NOT EDIT.
// +build portal x11
//go:build portal || x11
package main
package inputcontrol
var keysymsMap = map[rune]Keysym{
0x0008: 0x0000ff08,

View file

@ -1,4 +1,4 @@
// +build ignore
//go:build ignore
/*
* Copyright (c) 2018 Unrud <unrud@outlook.com>
@ -102,8 +102,8 @@ func main() {
keysymsMap[unicode] = keysym
}
content := "// Code generated by keysyms.generator.go. DO NOT EDIT.\n" +
"// +build portal x11\n\n" +
"package main\n\n" +
"//go:build portal || x11\n\n" +
"package inputcontrol\n\n" +
"var keysymsMap = map[rune]Keysym{\n"
keys := make([]rune, 0)
for key := range keysymsMap {

View file

@ -1,4 +1,4 @@
// +build portal x11
//go:build portal || x11
/*
* Copyright (c) 2018 Unrud <unrud@outlook.com>
@ -19,17 +19,26 @@
* along with Remote-Touchpad. If not, see <http://www.gnu.org/licenses/>.
*/
package main
package inputcontrol
//go:generate go run keysyms.generator.go
import "errors"
import "fmt"
type Keysym int32
const (
// X11/keysymdef.h
xkSuperL Keysym = 0xffeb
xkBackSpace Keysym = 0xff08
xkReturn Keysym = 0xff0D
xkDelete Keysym = 0xffff
xkHome Keysym = 0xff50
xkLeft Keysym = 0xff51
xkUp Keysym = 0xff52
xkRight Keysym = 0xff53
xkDown Keysym = 0xff54
xkEnd Keysym = 0xff57
xkSuperL Keysym = 0xffeb
// X11/XF86keysym.h
xf86xkAudioLowerVolume Keysym = 0x1008ff11
xf86xkAudioMute Keysym = 0x1008ff12
@ -48,8 +57,8 @@ func RuneToKeysym(runeValue rune) (Keysym, error) {
keysym, found := keysymsMap[runeValue]
if !found {
if runeValue < 0x100 || runeValue > 0x10ffff {
return 0, errors.New("rune not mappend to keysym and " +
"out of range for direct unicode mapping")
return 0, fmt.Errorf("rune not mappend to keysym and "+
"out of range for direct unicode mapping: %q", runeValue)
}
keysym = Keysym(0x01000000 + runeValue)
}
@ -57,32 +66,44 @@ func RuneToKeysym(runeValue rune) (Keysym, error) {
}
func KeyToKeysym(key Key) (Keysym, error) {
if key == KeySuper {
switch key {
case KeyBackSpace:
return xkBackSpace, nil
case KeyReturn:
return xkReturn, nil
case KeyDelete:
return xkDelete, nil
case KeyHome:
return xkHome, nil
case KeyLeft:
return xkLeft, nil
case KeyUp:
return xkUp, nil
case KeyRight:
return xkRight, nil
case KeyDown:
return xkDown, nil
case KeyEnd:
return xkEnd, nil
case KeySuper:
return xkSuperL, nil
}
if key == KeyVolumeMute {
case KeyVolumeMute:
return xf86xkAudioMute, nil
}
if key == KeyVolumeDown {
case KeyVolumeDown:
return xf86xkAudioLowerVolume, nil
}
if key == KeyVolumeUp {
case KeyVolumeUp:
return xf86xkAudioRaiseVolume, nil
}
if key == KeyMediaPlayPause {
case KeyMediaPlayPause:
return xf86xkAudioPlay, nil
}
if key == KeyMediaPrevTrack {
case KeyMediaPrevTrack:
return xf86xkAudioPrev, nil
}
if key == KeyMediaNextTrack {
case KeyMediaNextTrack:
return xf86xkAudioNext, nil
}
if key == KeyBrowserBack {
case KeyBrowserBack:
return xf86xkBack, nil
}
if key == KeyBrowserForward {
case KeyBrowserForward:
return xf86xkForward, nil
default:
return 0, fmt.Errorf("key not mapped to keysym: %#v", key)
}
return 0, errors.New("key not mapped to keysym")
}

110
main.go
View file

@ -27,7 +27,6 @@ import (
"errors"
"flag"
"fmt"
"golang.org/x/net/websocket"
"log"
mathrand "math/rand"
"net"
@ -37,6 +36,10 @@ import (
"strings"
"time"
"unicode/utf8"
"github.com/unrud/remote-touchpad/inputcontrol"
"github.com/unrud/remote-touchpad/terminal"
"golang.org/x/net/websocket"
)
const (
@ -45,26 +48,31 @@ const (
authenticationRateBurst int = 10
challengeLength int = 8
defaultBind string = ":0"
version string = "1.0.3"
pretty_app_name string = "Remote Touchpad"
version string = "1.5.4"
prettyAppName string = "Remote Touchpad"
)
func processCommand(backend Backend, command string) error {
type config struct {
UpdateRate uint `json:"updateRate"`
ScrollSpeed float64 `json:"scrollSpeed"`
MoveSpeed float64 `json:"moveSpeed"`
MouseScrollSpeed float64 `json:"mouseScrollSpeed"`
MouseMoveSpeed float64 `json:"mouseMoveSpeed"`
}
func processCommand(controller inputcontrol.Controller, command string) error {
if len(command) == 0 {
return errors.New("empty command")
}
if command == "sf" {
return backend.PointerScrollFinish()
if command == "S" {
return controller.PointerScroll(0, 0, true)
}
if command[0] == 't' {
text := command[1:]
// normalize line endings
text = strings.Replace(text, "\r\n", "\n", -1)
text = strings.Replace(text, "\r", "\n", -1)
if !utf8.ValidString(text) {
return errors.New("invalid utf-8")
}
return backend.KeyboardText(text)
return controller.KeyboardText(text)
}
arguments := strings.Split(command[1:], ";")
if command[0] == 'k' && len(arguments) != 1 ||
@ -76,30 +84,33 @@ func processCommand(backend Backend, command string) error {
return err
}
if command[0] == 'k' {
if x < 0 || x >= int64(KeyLimit) {
if x < 0 || x >= int64(inputcontrol.KeyLimit) {
return errors.New("unsupported key")
}
return backend.KeyboardKey(Key(x))
return controller.KeyboardKey(inputcontrol.Key(x))
}
y, err := strconv.ParseInt(arguments[1], 10, 32)
if err != nil {
return err
}
if command[0] == 'm' {
return backend.PointerMove(int(x), int(y))
return controller.PointerMove(int(x), int(y))
}
if command[0] == 's' {
return backend.PointerScroll(int(x), int(y))
return controller.PointerScroll(int(x), int(y), false)
}
if command[0] == 'S' {
return controller.PointerScroll(int(x), int(y), true)
}
if command[0] == 'b' {
if x < 0 || x >= int64(PointerButtonLimit) {
if x < 0 || x >= int64(inputcontrol.PointerButtonLimit) {
return errors.New("unsupported pointer button")
}
b := true
if y == 0 {
b = false
}
return backend.PointerButton(PointerButton(x), b)
return controller.PointerButton(inputcontrol.PointerButton(x), b)
}
return errors.New("unsupported command")
}
@ -140,14 +151,20 @@ func secureRandBase64(length int) string {
}
func main() {
TerminalSetTitle(pretty_app_name)
terminal.SetTitle(prettyAppName)
var bind, certFile, keyFile, secret string
var showVersion bool
var config config
flag.BoolVar(&showVersion, "version", false, "show program's version number and exit")
flag.StringVar(&bind, "bind", defaultBind, "bind server to [HOSTNAME]:PORT")
flag.StringVar(&secret, "secret", "", "shared secret for client authentication")
flag.StringVar(&certFile, "cert", "", "file containing TLS certificate")
flag.StringVar(&keyFile, "key", "", "file containing TLS private key")
flag.UintVar(&config.UpdateRate, "update-rate", 30, "number of updates per second")
flag.Float64Var(&config.MoveSpeed, "move-speed", 1, "move speed multiplier")
flag.Float64Var(&config.ScrollSpeed, "scroll-speed", 1, "scroll speed multiplier")
flag.Float64Var(&config.MouseMoveSpeed, "mouse-move-speed", 1, "mouse move speed multiplier")
flag.Float64Var(&config.MouseScrollSpeed, "mouse-scroll-speed", 1, "mouse scroll speed multiplier")
flag.Parse()
if showVersion {
fmt.Println(version)
@ -163,25 +180,32 @@ func main() {
if secret == "" {
secret = secureRandBase64(defaultSecretLength)
}
var backend Backend
var backendName string
platformErrors := ""
for _, backendInfo := range Backends {
backendName = backendInfo.Name
if len(inputcontrol.Controllers) == 0 {
log.Fatal("compiled without controller")
}
var controller inputcontrol.Controller
var controllerName string
var platformErrs []error
for _, controllerInfo := range inputcontrol.Controllers {
controllerName = controllerInfo.Name
var err error
backend, err = backendInfo.Init()
controller, err = controllerInfo.Init()
if err == nil {
break
} else if _, ok := err.(UnsupportedPlatformError); ok {
platformErrors += fmt.Sprintf("%s backend: %v\n", backendName, err)
} else {
log.Fatal(fmt.Sprintf("%s backend: %v", backendName, err))
var unsupportedErr *inputcontrol.UnsupportedPlatformError
wrappedErr := fmt.Errorf("%v controller: %w", controllerName, err)
if errors.As(err, &unsupportedErr) {
platformErrs = append(platformErrs, wrappedErr)
} else {
log.Fatal(wrappedErr)
}
}
}
if backend == nil {
log.Fatal("unsupported platform:\n" + platformErrors)
if controller == nil {
log.Fatal(fmt.Errorf("unsupported platform:\n%w", errors.Join(platformErrs...)))
}
defer backend.Close()
defer controller.Close()
authenticationChallenges := make(chan challenge, authenticationRateBurst)
go authenticationChallengeGenerator(secret, authenticationChallenges)
listener, err := net.Listen("tcp", bind)
@ -201,11 +225,11 @@ func main() {
}
}
if host == "" {
host = FindDefaultHost()
host = findDefaultHost()
}
port := addr.Port
mux := http.NewServeMux()
mux.Handle("/", http.FileServer(assetFS()))
mux.Handle("/", http.FileServer(http.FS(webdataFS)))
mux.Handle("/ws", websocket.Handler(func(ws *websocket.Conn) {
var message string
challenge := <-authenticationChallenges
@ -216,12 +240,13 @@ func main() {
if !challenge.verify(message) {
return
}
websocket.JSON.Send(ws, config)
for {
if err := websocket.Message.Receive(ws, &message); err != nil {
return
}
if err := processCommand(backend, message); err != nil {
log.Print(fmt.Sprintf("%s backend: %v", backendName, err))
if err := processCommand(controller, message); err != nil {
log.Print(fmt.Errorf("%s controller: %w", controllerName, err))
return
}
}
@ -234,16 +259,17 @@ func main() {
if tls {
scheme = "https"
}
url := fmt.Sprintf("%s://%s/#%s\n", scheme, domain, secret)
readyMsg := "ready: " + url
qrCode, _ := GenerateQRCode(url, TerminalSupportsColor(os.Stderr.Fd()))
readyMsg += qrCode
if !tls {
readyMsg += ("" +
"▌ WARNING: TLS is not enabled ▐\n" +
"▌Don't use in an untrusted network!▐\n")
url := fmt.Sprintf("%s://%s/#%s", scheme, domain, secret)
fmt.Println(url)
if qrCode, err := terminal.GenerateQRCode(url, terminal.SupportsColor(os.Stdout.Fd())); err == nil {
fmt.Print(qrCode)
} else {
log.Printf("QR code error: %v", err)
}
if !tls {
fmt.Println("▌ WARNING: TLS is not enabled ▐")
fmt.Println("▌Don't use in an untrusted network!▐")
}
log.Print(readyMsg)
if tls {
err = http.ServeTLS(listener, mux, certFile, keyFile)
} else {

12
remote-touchpad.service Normal file
View 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
View 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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 132 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 152 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 133 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Before After
Before After

View file

@ -1,17 +1,21 @@
# Build by running "snapcraft".
# Build by running `snapcraft`.
# WARNING:
# Snapcraft uses caching for already build steps but it's buggy and can cause strange problems.
# Clean the cache by running "snapcraft clean".
# Clean the cache by running `snapcraft clean`.
name: remote-touchpad
license: GPL-3.0+
grade: stable
adopt-info: remote-touchpad
base: core18
base: core24
confinement: strict
platforms:
amd64:
arm64:
apps:
remote-touchpad:
command: bin/remote-touchpad
@ -20,13 +24,13 @@ apps:
- desktop
- x11
remote-touchpad-wait-on-error:
command: usr/bin/remote-touchpad-wait-on-error
command: bin/remote-touchpad-wait-on-error
plugs:
- network-bind
- desktop
- x11
common-id: com.github.unrud.RemoteTouchpad
desktop: share/applications/com.github.unrud.RemoteTouchpad.desktop
desktop: usr/share/applications/com.github.unrud.RemoteTouchpad.desktop
parts:
remote-touchpad:
@ -36,18 +40,22 @@ parts:
- x11
source: .
source-type: git
build-snaps:
- go
build-packages:
- gcc
- libc6-dev
- libxt-dev
- libxrandr-dev
- libxtst-dev
stage-packages:
- libxt6
- libxrandr2
- libxtst6
override-build: |
snapcraftctl build
install -Dm0755 -t "${SNAPCRAFT_PART_INSTALL}/usr/bin" desktop/remote-touchpad-wait-on-error
install -Dm0644 -t "${SNAPCRAFT_PART_INSTALL}/usr/share/metainfo" desktop/com.github.unrud.RemoteTouchpad.metainfo.xml
install -Dm0644 -t "${SNAPCRAFT_PART_INSTALL}/usr/share/applications" desktop/com.github.unrud.RemoteTouchpad.desktop
install -Dm0644 -t "${SNAPCRAFT_PART_INSTALL}/usr/share/icons/hicolor/scalable/apps" desktop/com.github.unrud.RemoteTouchpad.svg
craftctl default
install -Dm0755 -t "${CRAFT_PART_INSTALL}/bin" desktop/remote-touchpad-wait-on-error
install -Dm0644 -t "${CRAFT_PART_INSTALL}/usr/share/metainfo" desktop/com.github.unrud.RemoteTouchpad.metainfo.xml
install -Dm0644 -t "${CRAFT_PART_INSTALL}/usr/share/applications" desktop/com.github.unrud.RemoteTouchpad.desktop
install -Dm0644 -t "${CRAFT_PART_INSTALL}/usr/share/icons/hicolor/scalable/apps" desktop/com.github.unrud.RemoteTouchpad.svg
parse-info: [usr/share/metainfo/com.github.unrud.RemoteTouchpad.metainfo.xml]

View file

@ -17,7 +17,7 @@
* along with Remote-Touchpad. If not, see <http://www.gnu.org/licenses/>.
*/
package main
package terminal
import qrcode "github.com/skip2/go-qrcode"
@ -33,7 +33,7 @@ func qrCodeToString(bits [][]bool, colorize bool) string {
s := ""
for y := -1; y < len(bits); y += 2 {
if colorize {
s += TerminalForegroundWhite + TerminalBackgroundBlack
s += ForegroundBlack + BackgroundWhite
}
for x := range bits[0] {
upper := false
@ -45,17 +45,17 @@ func qrCodeToString(bits [][]bool, colorize bool) string {
lower = bits[y+1][x]
}
if upper && lower {
s += " "
} else if !upper && lower {
s += "▀"
s += "█"
} else if upper && !lower {
s += "▀"
} else if !upper && lower {
s += "▄"
} else {
s += ""
s += " "
}
}
if colorize {
s += TerminalForegroundReset + TerminalBackgroundReset
s += ForegroundReset + BackgroundReset
}
s += "\n"
}

View file

@ -17,11 +17,11 @@
* along with Remote-Touchpad. If not, see <http://www.gnu.org/licenses/>.
*/
package main
package terminal
const (
TerminalForegroundWhite string = "\033[37m"
TerminalForegroundReset string = "\033[39m"
TerminalBackgroundBlack string = "\033[40m"
TerminalBackgroundReset string = "\033[49m"
ForegroundBlack string = "\033[30m"
ForegroundReset string = "\033[39m"
BackgroundWhite string = "\033[47m"
BackgroundReset string = "\033[49m"
)

View file

@ -1,4 +1,4 @@
// +build !windows
//go:build !windows
/*
* Copyright (c) 2018-2019 Unrud <unrud@outlook.com>
@ -19,17 +19,17 @@
* along with Remote-Touchpad. If not, see <http://www.gnu.org/licenses/>.
*/
package main
package terminal
// #include <unistd.h>
import "C"
import "os"
func TerminalSupportsColor(fd uintptr) bool {
func SupportsColor(fd uintptr) bool {
return C.isatty(C.int(fd)) != 0
}
func TerminalSetTitle(title string) bool {
func SetTitle(title string) bool {
if C.isatty(C.int(os.Stdout.Fd())) != 0 {
os.Stdout.Write([]byte("\x1b]2;" + title + "\x07"))
os.Stdout.Sync()

View file

@ -17,7 +17,7 @@
* along with Remote-Touchpad. If not, see <http://www.gnu.org/licenses/>.
*/
package main
package terminal
import (
"syscall"
@ -36,13 +36,13 @@ var (
setConsoleTitleProc = kernel32DLL.NewProc("SetConsoleTitleW")
)
func TerminalSupportsColor(fd uintptr) bool {
func SupportsColor(fd uintptr) bool {
r, _, _ := setConsoleModeProc.Call(fd, uintptr(enableProcessedOuput|
enableWrapAtEolOutput|enableVirtualTerminalProcessing))
return r != 0
}
func TerminalSetTitle(title string) bool {
func SetTitle(title string) bool {
r, _, _ := setConsoleTitleProc.Call(
uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(title))))
return r != 0

View file

@ -1,7 +1,5 @@
// +build !portal
/*
* Copyright (c) 2018 Unrud <unrud@outlook.com>
* Copyright (c) 2024 Unrud <unrud@outlook.com>
*
* This file is part of Remote-Touchpad.
*
@ -21,8 +19,34 @@
package main
import "errors"
import (
"embed"
"io/fs"
"log"
"mime"
)
func InitPortalBackend() (Backend, error) {
return nil, UnsupportedPlatformError{errors.New("disabled")}
//go:embed webdata/*
var webdataFSWithPrefix embed.FS
var webdataFS fs.FS
var webdataTypes = map[string]string{
".css": "text/css; charset=utf-8",
".html": "text/html; charset=utf-8",
".mjs": "text/javascript; charset=utf-8",
".png": "image/png",
".woff": "font/woff",
}
func init() {
var err error
webdataFS, err = fs.Sub(webdataFSWithPrefix, "webdata")
if err != nil {
log.Fatal(err)
}
for ext, typ := range webdataTypes {
if err := mime.AddExtensionType(ext, typ); err != nil {
log.Fatal(err)
}
}
}

82
webdata/app/compat.mjs Normal file
View file

@ -0,0 +1,82 @@
/*
* Copyright (c) 2018-2019, 2023 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/>.
*/
export const fullscreenEnabled = () => {
return (document.fullscreenEnabled ||
document.webkitFullscreenEnabled ||
false);
};
export const requestFullscreen = (element, options) => {
if (element.requestFullscreen) {
element.requestFullscreen(options);
} else if (element.webkitRequestFullscreen) {
element.webkitRequestFullscreen(options);
}
};
export const exitFullscreen = () => {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
}
};
export const fullscreenElement = () => {
return (document.fullscreenElement ||
document.webkitFullscreenElement ||
null);
};
export const addFullscreenchangeEventListener = (listener) => {
if ("onfullscreenchange" in document) {
document.addEventListener("fullscreenchange", listener);
} else if ("onwebkitfullscreenchange" in document) {
document.addEventListener("webkitfullscreenchange", listener);
}
};
export const requestPointerLock = (element) => {
if (element.requestPointerLock) {
element.requestPointerLock();
}
};
export const exitPointerLock = () => {
if (document.exitPointerLock) {
document.exitPointerLock();
}
};
export const pointerLockElement = () => {
return document.pointerLockElement || null;
};
export const addPointerlockchangeEventListener = (listener) => {
if ("onpointerlockchange" in document) {
document.addEventListener("pointerlockchange", listener);
}
};
export const vibrate = (pattern) => {
if (navigator.vibrate) {
navigator.vibrate(pattern);
}
};

View file

@ -0,0 +1,121 @@
/*
* Copyright (c) 2018-2019, 2023 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/>.
*/
export const POINTER_BUTTON_LEFT = 0;
export const POINTER_BUTTON_RIGHT = 1;
export const POINTER_BUTTON_MIDDLE = 2;
export const KEY_VOLUME_MUTE = 0;
export const KEY_VOLUME_DOWN = 1;
export const KEY_VOLUME_UP = 2;
export const KEY_MEDIA_PLAY_PAUSE = 3;
export const KEY_MEDIA_PREV_TRACK = 4;
export const KEY_MEDIA_NEXT_TRACK = 5;
export const KEY_BROWSER_BACK = 6;
export const KEY_BROWSER_FORWARD = 7;
export const KEY_SUPER = 8;
export const KEY_LEFT = 9;
export const KEY_RIGHT = 10;
export const KEY_UP = 11;
export const KEY_DOWN = 12;
export const KEY_HOME = 13;
export const KEY_END = 14;
export const KEY_BACK_SPACE = 15;
export const KEY_DELETE = 16;
export const KEY_RETURN = 17;
export default class InputController {
#updateRate = 0;
#moveXSum = 0;
#moveYSum = 0;
#scrollHSum = 0;
#scrollVSum = 0;
#scrolling = false;
#scrollFinish = false;
#updateTimeoutActive = false;
#socket;
constructor(socket) {
this.#socket = socket;
}
configure(config) {
this.#updateRate = config.updateRate;
}
#startUpdate(fromTimeout) {
if (this.#updateTimeoutActive && !fromTimeout) {
return;
}
this.#updateTimeoutActive = false;
let finished = true;
const xInt = Math.trunc(this.#moveXSum);
const yInt = Math.trunc(this.#moveYSum);
if (xInt != 0 || yInt != 0) {
this.#socket.send("m" + xInt + ";" + yInt);
this.#moveXSum -= xInt;
this.#moveYSum -= yInt;
finished = false;
}
const hInt = Math.trunc(this.#scrollHSum);
const vInt = Math.trunc(this.#scrollVSum);
if (hInt != 0 || vInt != 0) {
this.#socket.send((this.#scrollFinish ? "S" : "s") + hInt + ";" + vInt);
this.#scrollHSum -= hInt;
this.#scrollVSum -= vInt;
this.#scrolling = !this.#scrollFinish;
this.#scrollFinish = false;
finished = false;
} else if (this.#scrollFinish && this.#scrolling) {
this.#socket.send("S");
this.#scrolling = false;
this.#scrollFinish = false;
}
this.#updateTimeoutActive = !finished && this.#updateRate > 0;
if (this.#updateTimeoutActive) {
setTimeout(this.#startUpdate.bind(this), 1000 / this.#updateRate, true);
}
}
pointerMove(deltaX, deltaY) {
this.#moveXSum += deltaX;
this.#moveYSum += deltaY;
this.#startUpdate();
}
pointerScroll(deltaHorizontal, deltaVertical, finish) {
this.#scrollHSum += deltaHorizontal;
this.#scrollVSum += deltaVertical;
this.#scrollFinish |= finish;
this.#startUpdate();
};
pointerButton(button, press) {
this.#socket.send("b" + button + ";" + (press ? 1 : 0));
}
keyboardKey(key) {
this.#socket.send("k" + key);
}
keyboardText(text) {
this.#socket.send("t" + text);
}
}

74
webdata/app/keyboard.mjs Normal file
View file

@ -0,0 +1,74 @@
/*
* Copyright (c) 2018-2019, 2023 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/>.
*/
import {
KEY_SUPER, KEY_BACK_SPACE, KEY_RETURN, KEY_DELETE, KEY_HOME, KEY_END,
KEY_LEFT, KEY_RIGHT, KEY_UP, KEY_DOWN,
} from "./inputcontroller.mjs";
export default class Keyboard {
#inputController;
#checkAllowedCallback;
constructor(inputController, checkAllowedCallback) {
this.#inputController = inputController;
this.#checkAllowedCallback = checkAllowedCallback;
document.addEventListener("keydown", this.#handleKeydown.bind(this));
}
configure() {}
#handleKeydown(event) {
if (!this.#checkAllowedCallback() ||
event.ctrlKey || event.altKey || event.isComposing) {
return;
}
let key = null;
if (event.key == "OS" || event.key == "Super" || event.key == "Meta") {
key = KEY_SUPER;
} else if (event.key == "Backspace") {
key = KEY_BACK_SPACE;
} else if (event.key == "Enter") {
key = KEY_RETURN;
} else if (event.key == "Delete") {
key = KEY_DELETE;
} else if (event.key == "Home") {
key = KEY_HOME;
} else if (event.key == "End") {
key = KEY_END;
} else if (event.key == "Left" || event.key == "ArrowLeft") {
key = KEY_LEFT;
} else if (event.key == "Right" || event.key == "ArrowRight") {
key = KEY_RIGHT;
} else if (event.key == "Up" || event.key == "ArrowUp") {
key = KEY_UP;
} else if (event.key == "Down" || event.key == "ArrowDown") {
key = KEY_DOWN;
}
if (key != null) {
if (!event.shiftKey) {
event.preventDefault();
this.#inputController.keyboardKey(key);
}
} else if (event.key.length == 1) {
event.preventDefault();
this.#inputController.keyboardText(event.key);
}
}
}

53
webdata/app/main.mjs Normal file
View file

@ -0,0 +1,53 @@
/*
* Copyright (c) 2018-2019, 2023 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/>.
*/
import InputController, * as inputcontrollerModule from "./inputcontroller.mjs";
import Socket from "./socket.mjs";
import UI from "./ui.mjs";
const url = new URL("ws", location.href);
url.protocol = url.protocol == "http:" ? "ws:" : "wss:";
const socket = new Socket(url, window.location.hash.substr(1));
const inputController = new InputController(socket);
const ui = new UI(inputController);
socket.addEventListener("config", (event) => {
const config = event.detail;
inputController.configure(config);
ui.configure(config);
});
socket.addEventListener("close", () => {
ui.close();
});
window.app = {
key: inputController.keyboardKey.bind(inputController),
text: inputController.keyboardText.bind(inputController),
toggleFullscreen: ui.toggleFullscreen.bind(ui),
showTextInput: ui.showTextInput.bind(ui),
showKeys: ui.showKeys.bind(ui),
setKeysPage: ui.setKeysPage.bind(ui),
};
for (const name in inputcontrollerModule) {
if (name.startsWith("KEY_")) {
window.app[name] = inputcontrollerModule[name];
}
}

74
webdata/app/mouse.mjs Normal file
View file

@ -0,0 +1,74 @@
/*
* Copyright (c) 2018-2019, 2023 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/>.
*/
export default class Mouse {
#moveSpeed = 1;
#scrollSpeed = 1;
#buttons = 0;
#inputController;
constructor (inputController, element) {
this.#inputController = inputController;
for (const type of ["touchstart", "touchend", "touchcancel", "touchmove"]) {
element.addEventListener(type, (event) => {
event.preventDefault();
});
}
element.addEventListener("mousedown", this.#handleMouseDownAndUp.bind(this));
element.addEventListener("mouseup", this.#handleMouseDownAndUp.bind(this));
element.addEventListener("mousemove", this.#handleMousemove.bind(this));
element.addEventListener("wheel", this.#handleWheel.bind(this));
}
configure(config) {
this.#moveSpeed = config.mouseMoveSpeed;
this.#scrollSpeed = config.mouseScrollSpeed;
}
#updateButtons(newButtons) {
for (let button = 0; button < 3; button += 1) {
const flag = 1 << button;
if ((newButtons&flag) != (this.#buttons & flag)) {
this.#inputController.pointerButton(button, newButtons & flag);
}
}
this.#buttons = newButtons;
}
#handleMouseDownAndUp(event) {
this.#updateButtons(event.buttons);
}
#handleMousemove(event) {
this.#inputController.pointerMove(
event.movementX * this.#moveSpeed, event.movementY * this.#moveSpeed);
}
#handleWheel(event) {
if (event.deltaMode == WheelEvent.DOM_DELTA_PIXEL) {
this.#inputController.pointerScroll(
event.deltaX * this.#scrollSpeed, event.deltaY * this.#scrollSpeed, true);
} else if (event.deltaMode == WheelEvent.DOM_DELTA_LINE) {
this.#inputController.pointerScroll(
event.deltaX * 20 * this.#scrollSpeed, event.deltaY * 20 * this.#scrollSpeed,
true);
}
}
}

9
webdata/app/sha256.mjs Normal file

File diff suppressed because one or more lines are too long

66
webdata/app/socket.mjs Normal file
View file

@ -0,0 +1,66 @@
/*
* Copyright (c) 2018-2019, 2023 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/>.
*/
import jsSHA from "./sha256.mjs"
const challengeResponse = (message, secret) => {
const shaObj = new jsSHA("SHA-256", "TEXT");
shaObj.setHMACKey(message, "TEXT");
shaObj.update(secret);
return btoa(shaObj.getHMAC("BYTES"));
};
export default class Socket extends EventTarget {
#secret;
#authenticated;
#ws;
constructor(url, secret) {
super();
this.#secret = secret;
this.#authenticated = false;
this.#ws = new WebSocket(url);
this.#ws.addEventListener("message", this.#handle_ws_message.bind(this));
this.#ws.addEventListener("close", this.#handle_ws_close.bind(this));
}
#handle_ws_message(event) {
if (!this.#authenticated) {
this.#ws.send(challengeResponse(event.data, this.#secret));
this.#authenticated = true;
return;
}
let config;
try {
config = JSON.parse(event.data);
} catch (e) {
this.#ws.close();
throw (e);
}
this.dispatchEvent(new CustomEvent("config", {detail: config}));
}
#handle_ws_close() {
this.dispatchEvent(new CustomEvent("close"));
}
send(message) {
this.#ws.send(message);
}
}

232
webdata/app/touchpad.mjs Normal file
View file

@ -0,0 +1,232 @@
/*
* Copyright (c) 2018-2019, 2023 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/>.
*/
import {POINTER_BUTTON_LEFT, POINTER_BUTTON_MIDDLE, POINTER_BUTTON_RIGHT} from "./inputcontroller.mjs";
// [1 Touch, 2 Touches, 3 Touches] (as pixel)
const TOUCH_MOVE_THRESHOLD = [10, 15, 15];
// Max time between consecutive touches for clicking or dragging (as milliseconds)
const TOUCH_TIMEOUT = 250;
// [[pixel/second, multiplicator], ...]
const POINTER_ACCELERATION = [
[0, 0],
[87, 1],
[173, 1],
[553, 2],
];
const copyTouch = (touch, timeStamp) => ({
identifier: touch.identifier,
pageX: touch.pageX,
pageXStart: touch.pageX,
pageY: touch.pageY,
pageYStart: touch.pageY,
timeStamp: timeStamp,
});
const calculateAccelerationMult = (speed) => {
for (let i = 0; i < POINTER_ACCELERATION.length; i += 1) {
const s2 = POINTER_ACCELERATION[i][0];
const a2 = POINTER_ACCELERATION[i][1];
if (s2 <= speed) {
continue;
}
if (i == 0) {
return a2;
}
const s1 = POINTER_ACCELERATION[i - 1][0];
const a1 = POINTER_ACCELERATION[i - 1][1];
return ((speed - s1) / (s2 - s1)) * (a2 - a1) + a1;
}
if (POINTER_ACCELERATION.length > 0) {
return POINTER_ACCELERATION[POINTER_ACCELERATION.length - 1][1];
}
return 1;
};
export default class Touchpad {
#moveSpeed = 1;
#scrollSpeed = 1;
#moved = false;
#startTimeStamp = 0;
#lastEndTimeStamp = 0;
#releasedCount = 0;
#ongoingTouches = [];
#dragging = false;
#draggingTimeout = null;
#inputController;
#checkAllowedCallback;
constructor(inputController, checkAllowedCallback) {
this.#inputController = inputController;
this.#checkAllowedCallback = checkAllowedCallback;
document.addEventListener("touchstart", this.#handleTouchstart.bind(this));
document.addEventListener("touchend", this.#handleTouchend.bind(this));
document.addEventListener("touchcancel", this.#handleTouchend.bind(this));
document.addEventListener("touchmove", this.#handleTouchmove.bind(this));
}
configure(config) {
this.#moveSpeed = config.moveSpeed;
this.#scrollSpeed = config.scrollSpeed;
}
#ongoingTouchIndexById(idToFind) {
for (let i = 0; i < this.#ongoingTouches.length; i += 1) {
if (this.#ongoingTouches[i].identifier == idToFind) {
return i;
}
}
return -1;
}
#handleDraggingTimeout() {
this.#draggingTimeout = null;
this.#inputController.pointerButton(POINTER_BUTTON_LEFT, false);
}
#handleTouchstart(event) {
// Might get called multiple times for the same touches
if (this.#ongoingTouches.length == 0) {
this.#startTimeStamp = event.timeStamp;
this.#moved = false;
}
const touches = event.changedTouches;
let foundTouch = false;
for (let i = 0; i < touches.length; i += 1) {
if (this.#ongoingTouches.length == 0 &&
!this.#checkAllowedCallback(touches[i].target)) {
continue;
}
foundTouch = true;
const touch = copyTouch(touches[i], event.timeStamp);
const idx = this.#ongoingTouchIndexById(touch.identifier);
if (idx < 0) {
this.#ongoingTouches.push(touch);
} else {
this.#ongoingTouches[idx] = touch;
}
}
if (!foundTouch) {
return;
}
event.preventDefault();
this.#lastEndTimeStamp = 0;
if (this.#draggingTimeout != null) {
clearTimeout(this.#draggingTimeout);
this.#draggingTimeout = null;
this.#dragging = true;
}
this.#inputController.pointerScroll(0, 0, true);
}
#handleTouchend(event) {
const touches = event.changedTouches;
let foundTouch = false;
for (let i = 0; i < touches.length; i += 1) {
const idx = this.#ongoingTouchIndexById(touches[i].identifier);
if (idx < 0) {
continue;
}
foundTouch = true;
this.#ongoingTouches.splice(idx, 1);
this.#releasedCount += 1;
}
if (!foundTouch) {
return;
}
event.preventDefault();
this.#lastEndTimeStamp = event.timeStamp;
this.#inputController.pointerScroll(0, 0, true);
if (this.#releasedCount > TOUCH_MOVE_THRESHOLD.length) {
this.#moved = true;
}
if (this.#ongoingTouches.length == 0 && this.#releasedCount >= 1) {
if (this.#dragging) {
this.#dragging = false;
this.#inputController.pointerButton(POINTER_BUTTON_LEFT, false);
}
if (!this.#moved && event.timeStamp - this.#startTimeStamp < TOUCH_TIMEOUT) {
let button = 0;
if (this.#releasedCount == 1) {
button = POINTER_BUTTON_LEFT;
} else if (this.#releasedCount == 2) {
button = POINTER_BUTTON_RIGHT;
} else if (this.#releasedCount == 3) {
button = POINTER_BUTTON_MIDDLE;
}
this.#inputController.pointerButton(button, true);
if (button == POINTER_BUTTON_LEFT) {
this.#draggingTimeout = setTimeout(
this.#handleDraggingTimeout.bind(this), TOUCH_TIMEOUT);
} else {
this.#inputController.pointerButton(button, false);
}
}
this.#releasedCount = 0;
}
}
#handleTouchmove(event) {
let sumX = 0;
let sumY = 0;
const touches = event.changedTouches;
let foundTouch = false;
for (let i = 0; i < touches.length; i += 1) {
const idx = this.#ongoingTouchIndexById(touches[i].identifier);
if (idx < 0) {
continue;
}
foundTouch = true;
if (!this.#moved) {
const dist = Math.sqrt(
Math.pow(touches[i].pageX - this.#ongoingTouches[idx].pageXStart, 2) +
Math.pow(touches[i].pageY - this.#ongoingTouches[idx].pageYStart, 2)
);
if (this.#ongoingTouches.length > TOUCH_MOVE_THRESHOLD.length ||
dist > TOUCH_MOVE_THRESHOLD[this.#ongoingTouches.length - 1] ||
event.timeStamp - this.#startTimeStamp >= TOUCH_TIMEOUT) {
this.#moved = true;
}
}
const dx = touches[i].pageX - this.#ongoingTouches[idx].pageX;
const dy = touches[i].pageY - this.#ongoingTouches[idx].pageY;
const timeDelta = event.timeStamp - this.#ongoingTouches[idx].timeStamp;
sumX += dx * calculateAccelerationMult(Math.abs(dx) / timeDelta * 1000);
sumY += dy * calculateAccelerationMult(Math.abs(dy) / timeDelta * 1000);
this.#ongoingTouches[idx].pageX = touches[i].pageX;
this.#ongoingTouches[idx].pageY = touches[i].pageY;
this.#ongoingTouches[idx].timeStamp = event.timeStamp;
}
if (!foundTouch) {
return;
}
event.preventDefault();
if (this.#moved && event.timeStamp - this.#lastEndTimeStamp >= TOUCH_TIMEOUT) {
if (this.#ongoingTouches.length == 1 || this.#dragging) {
this.#inputController.pointerMove(
sumX * this.#moveSpeed, sumY * this.#moveSpeed);
} else if (this.#ongoingTouches.length == 2) {
this.#inputController.pointerScroll(
-sumX * this.#scrollSpeed, -sumY * this.#scrollSpeed, false);
}
}
}
}

199
webdata/app/ui.mjs Normal file
View file

@ -0,0 +1,199 @@
/*
* Copyright (c) 2018-2019, 2023 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/>.
*/
import Keyboard from "./keyboard.mjs";
import Mouse from "./mouse.mjs";
import Touchpad from "./touchpad.mjs";
import * as compat from "./compat.mjs";
const IGNORE_CLICK_AFTER_TOUCH_DURATION = 1000; // milliseconds
const CLICK_VIBRATION_PATTERN = [10];
const buttons = document.querySelectorAll("button");
const scenes = document.querySelectorAll("body > .scene");
const openingScene = document.getElementById("opening");
const closedScene = document.getElementById("closed");
const padScene = document.getElementById("pad");
const keysScene = document.getElementById("keys");
const keysPages = keysScene.querySelectorAll(":scope > .page");
const textInputScene = document.getElementById("text-input");
const textInput = textInputScene.querySelector("textarea");
const mouseScene = document.getElementById("mouse");
const sendText = document.getElementById("send-text");
export default class UI {
#activeScene = null;
#keysActiveName = "";
#ready = false;
#closed = false;
#ignoreClickUntilTimeStamp = Number.MIN_VALUE;
#inputController;
#mouse;
#keyboard;
#touchpad;
constructor(inputController) {
this.#inputController = inputController;
this.#mouse = new Mouse(inputController, mouseScene);
this.#keyboard = new Keyboard(inputController,
() => this.#activeScene?.classList.contains("keyboard-input"));
this.#touchpad = new Touchpad(inputController,
(target) => target.classList.contains("touch-input"));
document.addEventListener("mousedown", this.#handleMousedown.bind(this));
document.addEventListener("touchend", this.#handleTouchend.bind(this));
textInput.addEventListener("input", () => { this.#updateTextInput(); });
sendText.addEventListener("click", this.#handleSendText.bind(this));
window.addEventListener("popstate", () => { this.#update(); });
compat.addFullscreenchangeEventListener(() => { this.#update(); });
compat.addPointerlockchangeEventListener(() => { this.#update(); });
for (const button of buttons) {
button.setAttribute("tabindex", "-1");
button.addEventListener("click", this.#handleButtonClick.bind(this));
}
this.#update();
}
configure(config) {
this.#mouse.configure(config);
this.#keyboard.configure(config);
this.#touchpad.configure(config);
if (!this.#closed) {
this.#ready = true;
}
this.#update();
}
close() {
this.#ready = false;
this.#closed = true;
this.#update();
}
#handleTouchend(event) {
// HACK: event.preventDefault doesn't reliably stop click events in Firefox (112)
this.#ignoreClickUntilTimeStamp = event.timeStamp + IGNORE_CLICK_AFTER_TOUCH_DURATION;
}
#handleMousedown(event) {
if (this.#activeScene != mouseScene && event.buttons == 1 &&
this.#ignoreClickUntilTimeStamp <= event.timeStamp &&
event.target.classList.contains("mouse-input")) {
compat.requestPointerLock(mouseScene);
}
}
#updateTextInput(newValue = null) {
if (newValue != null) {
textInput.value = newValue;
}
sessionStorage.setItem("text-input", textInput.value);
sendText.textContent = sendText.getAttribute(`data-${textInput.value ? "send" : "back"}-content`);
textInput.focus();
}
#handleButtonClick(event) {
event.target.classList.add("click");
setTimeout(() => event.target.classList.remove("click"), 0);
compat.vibrate(CLICK_VIBRATION_PATTERN);
}
#handleSendText() {
if (textInput.value) {
// normalize line endings
const text = textInput.value.replace(/\r\n?/g, "\n");
this.#inputController.keyboardText(text);
this.#updateTextInput("");
} else {
history.back();
}
}
#showScene(scene) {
this.#activeScene = scene;
if (compat.fullscreenElement() && !scene.classList.contains("allow-fullscreen")) {
compat.exitFullscreen();
}
if (compat.pointerLockElement() && this.#activeScene != mouseScene) {
compat.exitPointerLock();
}
textInput.value = "";
for (const otherScene of scenes) {
otherScene.classList.toggle("hidden", otherScene != scene);
}
}
setKeysPage(index, relative = false) {
if (relative) {
for (let i = 0; i < keysPages.length && keysPages[i].classList.contains("hidden");
i += 1, index += 1);
}
index = ((index % keysPages.length) + keysPages.length) % keysPages.length;
sessionStorage.setItem(this.#keysActiveName, index);
for (let i = 0; i < keysPages.length; i += 1) {
keysPages[i].classList.toggle("hidden", i != index);
}
}
showKeys(name = "", defaultPageIndex = 0) {
this.#showScene(keysScene);
this.#keysActiveName = "keys" + (name ? ":" + name : "");
let pageIndex = parseInt(sessionStorage.getItem(this.#keysActiveName));
if (isNaN(pageIndex)) {
pageIndex = defaultPageIndex;
}
this.setKeysPage(pageIndex);
if (history.state != this.#keysActiveName) {
history.pushState(this.#keysActiveName, "");
}
}
showTextInput() {
this.#showScene(textInputScene);
this.#updateTextInput(sessionStorage.getItem("text-input") || "");
if (history.state != "text-input") {
history.pushState("text-input", "");
}
}
toggleFullscreen() {
if (compat.fullscreenElement()) {
compat.exitFullscreen();
} else {
compat.requestFullscreen(document.documentElement, {navigationUI: "hide"});
}
}
#update() {
const fullscreenEnabled = compat.fullscreenEnabled();
for (const element of document.querySelectorAll(".visble-if-fullscreen-enabled")) {
element.classList.toggle("hidden", !fullscreenEnabled);
}
if (!this.#ready) {
this.#showScene(this.#closed ? closedScene : openingScene);
} else if (compat.pointerLockElement()) {
this.#showScene(mouseScene);
} else if ((history.state || "").split(":")[0] == "keys") {
this.showKeys(history.state.substr("keys:".length));
} else if (history.state == "text-input") {
this.showTextInput();
} else {
this.#showScene(padScene);
}
}
}

View file

@ -1,428 +0,0 @@
"use strict";
/*
* Copyright (c) 2018-2019 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/>.
*/
// [1 Touch, 2 Touches, 3 Touches] (as pixel)
const TOUCH_MOVE_THRESHOLD = [10, 15, 15];
// Max time between consecutive touches for clicking or dragging (as milliseconds)
const TOUCH_TIMEOUT = 250;
// [[pixel/second, multiplicator], ...]
const POINTER_ACCELERATION = [
[0, 0],
[87, 1],
[173, 1],
[553, 2]
];
const POINTER_BUTTON_LEFT = 0;
const POINTER_BUTTON_RIGHT = 1;
const POINTER_BUTTON_MIDDLE = 2;
const KEY_VOLUME_MUTE = 0;
const KEY_VOLUME_DOWN = 1;
const KEY_VOLUME_UP = 2;
const KEY_MEDIA_PLAY_PAUSE = 3;
const KEY_MEDIA_PREV_TRACK = 4;
const KEY_MEDIA_NEXT_TRACK = 5;
const KEY_BROWSER_BACK = 6;
const KEY_BROWSER_FORWARD = 7;
const KEY_SUPER = 8;
let ws;
let pad;
let padlabel;
let touchMoved = false;
let touchStart = 0;
let touchLastEnd = 0;
let touchReleasedCount = 0;
let ongoingTouches = [];
let moveXSum = 0;
let moveYSum = 0;
let scrollXSum = 0;
let scrollYSum = 0;
let dragging = false;
let draggingTimeout = null;
let scrolling = false;
function fullscreenEnabled() {
return (document.fullscreenEnabled ||
document.webkitFullscreenEnabled ||
document.mozFullScreenEnabled ||
document.msFullscreenEnabled ||
false);
}
function requestFullscreen(e, options) {
if (e.requestFullscreen) {
e.requestFullscreen(options);
} else if (e.webkitRequestFullscreen) {
e.webkitRequestFullscreen(options);
} else if (e.mozRequestFullScreen) {
e.mozRequestFullScreen(options);
} else if (e.msRequestFullscreen) {
e.msRequestFullscreen(options);
}
}
function exitFullscreen() {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
} else if (document.msExitFullscreen) {
document.msExitFullscreen();
}
}
function fullscreenElement() {
return (document.fullscreenElement ||
document.webkitFullscreenElement ||
document.mozFullScreenElement ||
document.msFullscreenElement ||
null);
}
function addFullscreenchangeEventListener(listener) {
if (document.fullscreenElement != undefined) {
document.addEventListener("fullscreenchange", listener);
} else if (document.webkitFullscreenElement != undefined) {
document.addEventListener("webkitfullscreenchange", listener);
} else if (document.mozFullScreenElement != undefined) {
document.addEventListener("mozfullscreenchange", listener);
} else if (document.msFullscreenElement != undefined) {
document.addEventListener("MSFullscreenChange", listener);
}
}
function copyTouch(touch, timeStamp) {
return {
identifier: touch.identifier,
pageX: touch.pageX,
pageXStart: touch.pageX,
pageY: touch.pageY,
pageYStart: touch.pageY,
timeStamp: timeStamp
};
}
function ongoingTouchIndexById(idToFind) {
for (let i = 0; i < ongoingTouches.length; i++) {
if (ongoingTouches[i].identifier == idToFind) {
return i;
}
}
return -1;
}
function calculatePointerAccelerationMult(speed) {
for (let i = 0; i < POINTER_ACCELERATION.length; i++) {
let s2 = POINTER_ACCELERATION[i][0];
let a2 = POINTER_ACCELERATION[i][1];
if (s2 <= speed) {
continue;
}
if (i == 0) {
return a2;
}
let s1 = POINTER_ACCELERATION[i - 1][0];
let a1 = POINTER_ACCELERATION[i - 1][1];
return ((speed - s1) / (s2 - s1)) * (a2 - a1) + a1;
}
if (POINTER_ACCELERATION.length > 0) {
return POINTER_ACCELERATION[POINTER_ACCELERATION.length - 1][1];
}
return 1;
}
function onDraggingTimeout() {
draggingTimeout = null;
ws.send("b" + POINTER_BUTTON_LEFT + ";0");
}
function updateMoveAndScroll() {
let moveX = Math.trunc(moveXSum);
let moveY = Math.trunc(moveYSum);
if (Math.abs(moveX) >= 1 || Math.abs(moveY) >= 1) {
moveXSum -= moveX;
moveYSum -= moveY;
ws.send("m" + moveX + ";" + moveY);
}
let scrollX = Math.trunc(scrollXSum);
let scrollY = Math.trunc(scrollYSum);
if (Math.abs(scrollX) >= 1 || Math.abs(scrollY) >= 1) {
scrollXSum -= scrollX;
scrollYSum -= scrollY;
scrolling = true;
ws.send("s" + scrollX + ";" + scrollY);
}
}
function handleStart(evt) {
// Might get called multiple times for the same touches
if (ongoingTouches.length == 0) {
touchStart = evt.timeStamp;
touchMoved = false;
}
let touches = evt.changedTouches;
for (let i = 0; i < touches.length; i++) {
if (touches[i].target != pad && touches[i].target != padlabel &&
ongoingTouches.length == 0) {
continue;
}
evt.preventDefault();
let touch = copyTouch(touches[i], evt.timeStamp);
let idx = ongoingTouchIndexById(touch.identifier);
if (idx < 0) {
ongoingTouches.push(touch);
} else {
ongoingTouches[idx] = touch;
}
touchLastEnd = 0;
if (!dragging) {
moveXSum = Math.trunc(moveXSum);
moveYSum = Math.trunc(moveYSum);
}
scrollXSum = Math.trunc(scrollXSum);
scrollYSum = Math.trunc(scrollYSum);
if (draggingTimeout != null) {
clearTimeout(draggingTimeout);
draggingTimeout = null;
dragging = true;
}
if (scrolling) {
ws.send("sf");
scrolling = false;
}
}
}
function handleEnd(evt) {
let touches = evt.changedTouches;
for (let i = 0; i < touches.length; i++) {
let idx = ongoingTouchIndexById(touches[i].identifier);
if (idx < 0) {
continue;
}
ongoingTouches.splice(idx, 1);
touchReleasedCount++;
touchLastEnd = evt.timeStamp;
if (scrolling) {
ws.send("sf");
scrolling = false;
}
}
if (touchReleasedCount > TOUCH_MOVE_THRESHOLD.length) {
touchMoved = true;
}
if (ongoingTouches.length == 0 && touchReleasedCount >= 1) {
if (dragging) {
dragging = false;
ws.send("b" + POINTER_BUTTON_LEFT + ";0");
}
if (!touchMoved && evt.timeStamp - touchStart < TOUCH_TIMEOUT) {
let button = 0;
if (touchReleasedCount == 1) {
button = POINTER_BUTTON_LEFT;
} else if (touchReleasedCount == 2) {
button = POINTER_BUTTON_RIGHT;
} else if (touchReleasedCount == 3) {
button = POINTER_BUTTON_MIDDLE;
}
ws.send("b" + button + ";1");
if (button == POINTER_BUTTON_LEFT) {
draggingTimeout = setTimeout(onDraggingTimeout, TOUCH_TIMEOUT);
} else {
ws.send("b" + button + ";0");
}
}
touchReleasedCount = 0;
}
}
function handleMove(evt) {
let sumX = 0;
let sumY = 0;
let touches = evt.changedTouches;
for (let i = 0; i < touches.length; i++) {
let idx = ongoingTouchIndexById(touches[i].identifier);
if (idx < 0) {
continue;
}
if (!touchMoved) {
let dist = Math.sqrt(Math.pow(touches[i].pageX - ongoingTouches[idx].pageXStart, 2) +
Math.pow(touches[i].pageY - ongoingTouches[idx].pageYStart, 2));
if (ongoingTouches.length > TOUCH_MOVE_THRESHOLD.length ||
dist > TOUCH_MOVE_THRESHOLD[ongoingTouches.length - 1] ||
evt.timeStamp - touchStart >= TOUCH_TIMEOUT) {
touchMoved = true;
}
}
let dx = touches[i].pageX - ongoingTouches[idx].pageX;
let dy = touches[i].pageY - ongoingTouches[idx].pageY;
let timeDelta = evt.timeStamp - ongoingTouches[idx].timeStamp;
sumX += dx * calculatePointerAccelerationMult(Math.abs(dx) / timeDelta * 1000);
sumY += dy * calculatePointerAccelerationMult(Math.abs(dy) / timeDelta * 1000);
ongoingTouches[idx].pageX = touches[i].pageX;
ongoingTouches[idx].pageY = touches[i].pageY;
ongoingTouches[idx].timeStamp = evt.timeStamp;
}
if (touchMoved && evt.timeStamp - touchLastEnd >= TOUCH_TIMEOUT) {
if (ongoingTouches.length == 1 || dragging) {
moveXSum += sumX;
moveYSum += sumY;
} else if (ongoingTouches.length == 2) {
scrollXSum -= sumX;
scrollYSum -= sumY;
}
updateMoveAndScroll();
}
}
function challengeResponse(message) {
let shaObj = new jsSHA("SHA-256", "TEXT");
shaObj.setHMACKey(message, "TEXT");
shaObj.update(window.location.hash.substr(1));
return btoa(shaObj.getHMAC("BYTES"));
}
window.addEventListener("load", function() {
let authenticated = false;
let opening = document.getElementById("opening");
let closed = document.getElementById("closed");
pad = document.getElementById("pad");
padlabel = document.getElementById("padlabel");
let keys = document.getElementById("keys");
let keyboard = document.getElementById("keyboard");
let fullscreenbutton = document.getElementById("fullscreenbutton");
let text = document.getElementById("text");
function showScene(scene) {
[opening, closed, pad, keys, keyboard].forEach(function(e) {
e.classList.toggle("hidden", e != scene);
});
}
function showKeys() {
if (fullscreenElement()) {
exitFullscreen();
}
showScene(keys);
if (history.state != "keys") {
history.pushState("keys", "");
}
}
function showKeyboard() {
if (fullscreenElement()) {
exitFullscreen();
}
showScene(keyboard);
text.focus();
if (history.state != "keyboard") {
history.pushState("keyboard", "");
}
}
text.value = "";
showScene(opening);
ws = new WebSocket(
(location.protocol == "http:" ? "ws:" : "wss:") + "//" + location.hostname +
(location.port ? ":" + location.port : "") + "/ws"
);
ws.onmessage = function(evt) {
if (authenticated) {
ws.close();
return;
}
authenticated = true;
ws.send(challengeResponse(evt.data));
if (history.state == "keyboard") {
showKeyboard();
} else if (history.state == "keys") {
showKeys()
} else {
showScene(pad);
}
};
ws.onclose = function() {
if (fullscreenElement()) {
exitFullscreen();
}
showScene(closed);
};
document.getElementById("keysbutton").addEventListener("click", showKeys);
document.getElementById("keyboardbutton").addEventListener("click", showKeyboard);
if (!fullscreenEnabled()) {
fullscreenbutton.classList.add("hidden");
}
fullscreenbutton.addEventListener("click", function() {
if (fullscreenElement()) {
exitFullscreen();
} else {
requestFullscreen(document.documentElement, {navigationUI: "hide"});
}
});
[{id: "browserbackbutton", key: KEY_BROWSER_BACK},
{id: "superbutton", key: KEY_SUPER},
{id: "browserforwardbutton", key: KEY_BROWSER_FORWARD},
{id: "prevtrackbutton", key: KEY_MEDIA_PREV_TRACK},
{id: "playpausebutton", key: KEY_MEDIA_PLAY_PAUSE},
{id: "nexttrackbutton", key: KEY_MEDIA_NEXT_TRACK},
{id: "volumedownbutton", key: KEY_VOLUME_DOWN},
{id: "volumemutebutton", key: KEY_VOLUME_MUTE},
{id: "volumeupbutton", key: KEY_VOLUME_UP}].forEach(function(o) {
document.getElementById(o.id).addEventListener("click", function() {
ws.send("k" + o.key);
});
});
document.getElementById("sendbutton").addEventListener("click", function() {
if (text.value != "") {
ws.send("t" + text.value);
text.value = "";
}
window.history.back();
});
window.onpopstate = function() {
if (!pad.classList.contains("hidden") ||
!keyboard.classList.contains("hidden") ||
!keys.classList.contains("hidden")) {
if (history.state == "keys") {
showKeys();
} else if (history.state == "keyboard") {
showKeyboard();
} else {
showScene(pad);
}
}
};
document.getElementById("reloadbutton").addEventListener("click", function() {
location.reload();
});
pad.addEventListener("touchstart", handleStart);
pad.addEventListener("touchend", handleEnd);
pad.addEventListener("touchcancel", handleEnd);
pad.addEventListener("touchmove", handleMove);
});

Binary file not shown.

View file

@ -1,46 +1,100 @@
<!DOCTYPE html>
<html lang="en">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=0">
<script src="fn.js"></script>
<script src="sha256.js"></script>
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no, interactive-widget=resizes-content">
<script src="app/main.mjs" type="module"></script>
<title>Remote Touchpad</title>
<link href="main.css" media="screen" rel="stylesheet">
<link href="icon.png" type="image/png" rel="icon">
<div id="opening">
<div id="opening" class="scene">
<noscript><p>JavaScript is required!</p></noscript>
<p>Connecting…</p>
</div>
<div id="closed" class="hidden">
<div id="closed" class="scene">
<p>Disconnected</p>
<button id="reloadbutton"></button>
<button class="large" onclick="location.reload()"></button>
</div>
<div id="pad" class="hidden">
<p id="padlabel">Touchpad</p>
<button id="keysbutton"></button>
<button id="fullscreenbutton"></button>
<button id="keyboardbutton"></button>
<div id="pad" class="scene touch-input mouse-input keyboard-input allow-fullscreen">
<p class="background">Touchpad</p>
<button class="top left" onclick="app.showKeys()"></button>
<button class="bottom left visible-if-fullscreen-enabled" onclick="app.toggleFullscreen()"></button>
<button class="bottom right" onclick="app.showTextInput()"></button>
</div>
<div id="keyboard" class="hidden">
<textarea id="text" cols=1 rows=1></textarea>
<button id="sendbutton"></button>
</div>
<div id="keys" class="hidden">
<div id="text-input" class="scene">
<textarea class="grow" cols=1 rows=1></textarea>
<div class="buttons">
<button id="browserbackbutton"></button>
<button id="superbutton">🪟</button>
<button id="browserforwardbutton"></button>
</div>
<div class="buttons">
<button id="prevtrackbutton"></button>
<button id="playpausebutton"></button>
<button id="nexttrackbutton"></button>
</div>
<div class="buttons">
<button id="volumedownbutton">🔉</button>
<button id="volumemutebutton">🔇</button>
<button id="volumeupbutton">🔊</button>
<button onclick="app.showKeys('text-input', 1)"></button>
<button class="grow" id="send-text" data-send-content="➣" data-back-content="⯇"></button>
</div>
</div>
<div id="keys" class="scene touch-input mouse-input keyboard-input allow-fullscreen">
<div class="page">
<div class="buttons" style="grid-column: 1 / 4">
<button onclick="app.key(app.KEY_BROWSER_BACK)"></button>
<button onclick="app.key(app.KEY_SUPER)">🪟</button>
<button onclick="app.key(app.KEY_BROWSER_FORWARD)"></button>
</div>
<div class="buttons" style="grid-column: 1 / 4">
<button onclick="app.key(app.KEY_MEDIA_PREV_TRACK)"></button>
<button onclick="app.key(app.KEY_MEDIA_PLAY_PAUSE)"></button>
<button onclick="app.key(app.KEY_MEDIA_NEXT_TRACK)"></button>
</div>
<div class="buttons" style="grid-column: 1 / 4">
<button onclick="app.key(app.KEY_VOLUME_DOWN)">🔉</button>
<button onclick="app.key(app.KEY_VOLUME_MUTE)">🔇</button>
<button onclick="app.key(app.KEY_VOLUME_UP)">🔊</button>
</div>
</div>
<div class="page">
<div class="buttons" style="grid-column: 1 / 4">
<button onclick="app.key(app.KEY_BACK_SPACE)"></button>
<button onclick="app.key(app.KEY_RETURN)"></button>
<button onclick="app.key(app.KEY_DELETE)"></button>
</div>
<div class="buttons" style="grid-column: 1 / 4">
<button onclick="app.key(app.KEY_HOME)"></button>
<button onclick="app.key(app.KEY_UP)"></button>
<button onclick="app.key(app.KEY_END)"></button>
</div>
<div class="buttons" style="grid-column: 1 / 4">
<button onclick="app.key(app.KEY_LEFT)"></button>
<button onclick="app.key(app.KEY_DOWN)"></button>
<button onclick="app.key(app.KEY_RIGHT)"></button>
</div>
</div>
<div class="page">
<button onclick="app.key(app.KEY_BACK_SPACE)"></button>
<button onclick="app.text(this.textContent)">/</button>
<button onclick="app.text(this.textContent)">*</button>
<button onclick="app.text(this.textContent)">-</button>
<button style="grid-row: 2" onclick="app.text(this.textContent)">7</button>
<button onclick="app.text(this.textContent)">8</button>
<button onclick="app.text(this.textContent)">9</button>
<button onclick="app.text(this.textContent)">+</button>
<button style="grid-row: 3" onclick="app.text(this.textContent)">4</button>
<button onclick="app.text(this.textContent)">5</button>
<button onclick="app.text(this.textContent)">6</button>
<button onclick="app.text(this.textContent)">=</button>
<button style="grid-row: 4" onclick="app.text(this.textContent)">1</button>
<button onclick="app.text(this.textContent)">2</button>
<button onclick="app.text(this.textContent)">3</button>
<button style="grid-row: 4 / 6; grid-column: 4" onclick="app.key(app.KEY_RETURN)"></button>
<button style="grid-row: 5; grid-column: 1 / 3" onclick="app.text(this.textContent)">0</button>
<button onclick="app.text(this.textContent)"><script>document.write(Intl.NumberFormat().formatToParts(1.1).find(x => x.type === "decimal").value)</script></button>
</div>
<button class="top right" onclick="app.setKeysPage(1, true)"></button>
<button class="top left" onclick="history.back()"></button>
</div>
<div id="mouse" class="scene keyboard-input allow-fullscreen">
<p class="background">Mouse</p>
</div>

View file

@ -4,7 +4,7 @@
}
.hidden {
display: none;
display: none !important;
}
html {
@ -16,22 +16,27 @@ html {
background-color: #404040;
color: white;
background-image: url(tex.png);
touch-action: none;
overflow: hidden;
}
html,
body,
body > div {
.scene {
width: 100%;
height: 100%;
margin: 0;
}
body > div {
.scene {
position: relative;
display: flex;
flex-direction: column;
justify-content: center;
box-sizing: border-box;
align-items: center;
gap: 2rem;
padding: 0.5rem;
}
p {
@ -41,40 +46,7 @@ p {
text-align: center;
}
button {
padding: 0;
background-color: transparent;
border: 1px solid black;
height: 1.5em;
width: 2em;
border-radius: 2px;
font-size: 2rem;
color: white;
font-family: "icon";
overflow: hidden;
}
button:focus {
outline: none;
}
#opening,
#closed {
padding: 0 0.5rem;
}
#opening noscript p,
#closed p {
margin-bottom: 1em;
}
#reloadbutton {
font-size: 4rem;
margin: 0 auto;
max-width: 100%;
}
#padlabel {
p.background {
color: gray;
text-shadow:
-1px -1px 0 black,
@ -84,43 +56,94 @@ button:focus {
opacity: 0.08;
font-weight: bold;
font-size: 4rem;
pointer-events: none;
}
@media (max-width: 32rem) {
#padlabel {
p.background {
font-size: 13vw;
}
}
#pad > button {
.large {
font-size: 4rem;
}
button {
box-sizing: border-box;
padding: 0;
background-color: transparent;
border: 1px solid black;
height: 1.5em;
width: 2em;
min-width: 1.2em;
min-height: 1.2em;
max-width: 100%;
max-height: 100%;
border-radius: 2px;
font-size: 2rem;
color: white;
font-family: "icon";
overflow: hidden;
cursor: pointer;
transition: box-shadow 0.2s ease-out;
}
button:focus {
outline: none;
}
button.click {
box-shadow: 0 0 1rem white inset;
transition: none;
}
button.top,
button.bottom,
button.left,
button.right {
position: absolute;
max-width: 50%;
max-height: 50%;
}
#fullscreenbutton {
left: 0;
bottom: 0;
border-bottom: none;
border-left: none;
}
#keyboardbutton {
right: 0;
bottom: 0;
border-right: none;
border-bottom: none;
}
#keysbutton {
left: 0;
button.top {
top: 0;
border-top: none;
}
button.bottom {
bottom: 0;
border-bottom: none;
}
button.left {
left: 0;
border-left: none;
}
button.right {
right: 0;
border-right: none;
}
.buttons {
display: flex;
flex-direction: row;
min-height: 0;
min-width: 0;
}
.buttons > button:not(:last-child) {
border-right: none;
}
.scene > .grow,
.buttons > .grow {
flex-grow: 1;
}
textarea {
flex-grow: 1;
border: none;
resize: none;
font-size: 2rem;
@ -130,49 +153,76 @@ textarea {
padding: 0 0.5rem;
}
#sendbutton {
border-bottom: none;
border-right: none;
border-left: none;
width: auto;
height: 1.5em;
}
@media (min-aspect-ratio: 4/3) {
#keyboard {
flex-direction: row;
}
#sendbutton {
border-top: none;
border-left: 1px solid black;
width: 2em;
height: auto;
}
textarea:focus {
outline: none;
}
#keys {
padding: 0.5rem;
padding-bottom: 0;
justify-content: flex-end;
padding: 0;
justify-content: end;
}
#keys .buttons {
display: flex;
justify-content: center;
margin-bottom: 0.5rem;
flex-shrink: 1;
height: 6rem;
#keys .page {
display: grid;
gap: 0.5rem;
min-height: 0;
min-width: 0;
grid-auto-rows: minmax(min-content, 6rem);
grid-auto-columns: minmax(min-content, 8rem);
padding: 0.5rem;
}
#keys .buttons > button:not(:last-of-type) {
#keys .page * {
width: 100%;
height: 100%;
}
#text-input {
align-items: stretch;
padding: 0;
gap: 0;
flex-direction: row;
}
#text-input .buttons {
flex-direction: column;
}
#text-input .buttons > button {
border-top: none;
border-right: none;
}
#keys .buttons > button {
flex-shrink: 1;
height: 100%;
width: 8rem;
min-width: 0;
#text-input .buttons > button:last-child {
border-bottom: none;
}
/* Only Safari for iOS */
@supports (-webkit-touch-callout: none) {
/* Virtual keyboard can overlay content */
#text-input .buttons > button:last-child {
display: flex;
padding-top: 0.2em;
justify-content: center;
}
}
/* Portrait orientation (not for Safari on iOS) */
@supports not (-webkit-touch-callout: none) { @media (max-aspect-ratio: 4/3) {
#text-input {
flex-direction: column;
}
#text-input .buttons {
flex-direction: row;
}
#text-input .buttons > button {
border-top: 1px solid black;
border-bottom: none;
}
#text-input .buttons > button:first-child {
border-left: none;
}
} }

View file

@ -1,27 +0,0 @@
/*
A JavaScript implementation of the SHA family of hashes, as
defined in FIPS PUB 180-4 and FIPS PUB 202, as well as the corresponding
HMAC implementation as defined in FIPS PUB 198a
Copyright 2008-2018 Brian Turek, 1998-2009 Paul Johnston & Contributors
Distributed under the BSD License
See http://caligatio.github.com/jsSHA/ for more information
*/
'use strict';(function(I){function w(c,a,d){var l=0,b=[],g=0,f,n,k,e,h,q,y,p,m=!1,t=[],r=[],u,z=!1;d=d||{};f=d.encoding||"UTF8";u=d.numRounds||1;if(u!==parseInt(u,10)||1>u)throw Error("numRounds must a integer >= 1");if(0===c.lastIndexOf("SHA-",0))if(q=function(b,a){return A(b,a,c)},y=function(b,a,l,f){var g,e;if("SHA-224"===c||"SHA-256"===c)g=(a+65>>>9<<4)+15,e=16;else throw Error("Unexpected error in SHA-2 implementation");for(;b.length<=g;)b.push(0);b[a>>>5]|=128<<24-a%32;a=a+l;b[g]=a&4294967295;
b[g-1]=a/4294967296|0;l=b.length;for(a=0;a<l;a+=e)f=A(b.slice(a,a+e),f,c);if("SHA-224"===c)b=[f[0],f[1],f[2],f[3],f[4],f[5],f[6]];else if("SHA-256"===c)b=f;else throw Error("Unexpected error in SHA-2 implementation");return b},p=function(b){return b.slice()},"SHA-224"===c)h=512,e=224;else if("SHA-256"===c)h=512,e=256;else throw Error("Chosen SHA variant is not supported");else throw Error("Chosen SHA variant is not supported");k=B(a,f);n=x(c);this.setHMACKey=function(b,a,g){var e;if(!0===m)throw Error("HMAC key already set");
if(!0===z)throw Error("Cannot set HMAC key after calling update");f=(g||{}).encoding||"UTF8";a=B(a,f)(b);b=a.binLen;a=a.value;e=h>>>3;g=e/4-1;if(e<b/8){for(a=y(a,b,0,x(c));a.length<=g;)a.push(0);a[g]&=4294967040}else if(e>b/8){for(;a.length<=g;)a.push(0);a[g]&=4294967040}for(b=0;b<=g;b+=1)t[b]=a[b]^909522486,r[b]=a[b]^1549556828;n=q(t,n);l=h;m=!0};this.update=function(a){var c,f,e,d=0,p=h>>>5;c=k(a,b,g);a=c.binLen;f=c.value;c=a>>>5;for(e=0;e<c;e+=p)d+h<=a&&(n=q(f.slice(e,e+p),n),d+=h);l+=d;b=f.slice(d>>>
5);g=a%h;z=!0};this.getHash=function(a,f){var d,h,k,q;if(!0===m)throw Error("Cannot call getHash after setting HMAC key");k=C(f);switch(a){case "HEX":d=function(a){return D(a,e,k)};break;case "B64":d=function(a){return E(a,e,k)};break;case "BYTES":d=function(a){return F(a,e)};break;case "ARRAYBUFFER":try{h=new ArrayBuffer(0)}catch(v){throw Error("ARRAYBUFFER not supported by this environment");}d=function(a){return G(a,e)};break;default:throw Error("format must be HEX, B64, BYTES, or ARRAYBUFFER");
}q=y(b.slice(),g,l,p(n));for(h=1;h<u;h+=1)q=y(q,e,0,x(c));return d(q)};this.getHMAC=function(a,f){var d,k,t,u;if(!1===m)throw Error("Cannot call getHMAC without first setting HMAC key");t=C(f);switch(a){case "HEX":d=function(a){return D(a,e,t)};break;case "B64":d=function(a){return E(a,e,t)};break;case "BYTES":d=function(a){return F(a,e)};break;case "ARRAYBUFFER":try{d=new ArrayBuffer(0)}catch(v){throw Error("ARRAYBUFFER not supported by this environment");}d=function(a){return G(a,e)};break;default:throw Error("outputFormat must be HEX, B64, BYTES, or ARRAYBUFFER");
}k=y(b.slice(),g,l,p(n));u=q(r,x(c));u=y(k,e,h,u);return d(u)}}function m(){}function D(c,a,d){var l="";a/=8;var b,g;for(b=0;b<a;b+=1)g=c[b>>>2]>>>8*(3+b%4*-1),l+="0123456789abcdef".charAt(g>>>4&15)+"0123456789abcdef".charAt(g&15);return d.outputUpper?l.toUpperCase():l}function E(c,a,d){var l="",b=a/8,g,f,n;for(g=0;g<b;g+=3)for(f=g+1<b?c[g+1>>>2]:0,n=g+2<b?c[g+2>>>2]:0,n=(c[g>>>2]>>>8*(3+g%4*-1)&255)<<16|(f>>>8*(3+(g+1)%4*-1)&255)<<8|n>>>8*(3+(g+2)%4*-1)&255,f=0;4>f;f+=1)8*g+6*f<=a?l+="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".charAt(n>>>
6*(3-f)&63):l+=d.b64Pad;return l}function F(c,a){var d="",l=a/8,b,g;for(b=0;b<l;b+=1)g=c[b>>>2]>>>8*(3+b%4*-1)&255,d+=String.fromCharCode(g);return d}function G(c,a){var d=a/8,l,b=new ArrayBuffer(d),g;g=new Uint8Array(b);for(l=0;l<d;l+=1)g[l]=c[l>>>2]>>>8*(3+l%4*-1)&255;return b}function C(c){var a={outputUpper:!1,b64Pad:"=",shakeLen:-1};c=c||{};a.outputUpper=c.outputUpper||!1;!0===c.hasOwnProperty("b64Pad")&&(a.b64Pad=c.b64Pad);if("boolean"!==typeof a.outputUpper)throw Error("Invalid outputUpper formatting option");
if("string"!==typeof a.b64Pad)throw Error("Invalid b64Pad formatting option");return a}function B(c,a){var d;switch(a){case "UTF8":case "UTF16BE":case "UTF16LE":break;default:throw Error("encoding must be UTF8, UTF16BE, or UTF16LE");}switch(c){case "HEX":d=function(a,b,c){var f=a.length,d,k,e,h,q;if(0!==f%2)throw Error("String of HEX type must be in byte increments");b=b||[0];c=c||0;q=c>>>3;for(d=0;d<f;d+=2){k=parseInt(a.substr(d,2),16);if(isNaN(k))throw Error("String of HEX type contains invalid characters");
h=(d>>>1)+q;for(e=h>>>2;b.length<=e;)b.push(0);b[e]|=k<<8*(3+h%4*-1)}return{value:b,binLen:4*f+c}};break;case "TEXT":d=function(c,b,d){var f,n,k=0,e,h,q,m,p,r;b=b||[0];d=d||0;q=d>>>3;if("UTF8"===a)for(r=3,e=0;e<c.length;e+=1)for(f=c.charCodeAt(e),n=[],128>f?n.push(f):2048>f?(n.push(192|f>>>6),n.push(128|f&63)):55296>f||57344<=f?n.push(224|f>>>12,128|f>>>6&63,128|f&63):(e+=1,f=65536+((f&1023)<<10|c.charCodeAt(e)&1023),n.push(240|f>>>18,128|f>>>12&63,128|f>>>6&63,128|f&63)),h=0;h<n.length;h+=1){p=k+
q;for(m=p>>>2;b.length<=m;)b.push(0);b[m]|=n[h]<<8*(r+p%4*-1);k+=1}else if("UTF16BE"===a||"UTF16LE"===a)for(r=2,n="UTF16LE"===a&&!0||"UTF16LE"!==a&&!1,e=0;e<c.length;e+=1){f=c.charCodeAt(e);!0===n&&(h=f&255,f=h<<8|f>>>8);p=k+q;for(m=p>>>2;b.length<=m;)b.push(0);b[m]|=f<<8*(r+p%4*-1);k+=2}return{value:b,binLen:8*k+d}};break;case "B64":d=function(a,b,c){var f=0,d,k,e,h,q,m,p;if(-1===a.search(/^[a-zA-Z0-9=+\/]+$/))throw Error("Invalid character in base-64 string");k=a.indexOf("=");a=a.replace(/\=/g,
"");if(-1!==k&&k<a.length)throw Error("Invalid '=' found in base-64 string");b=b||[0];c=c||0;m=c>>>3;for(k=0;k<a.length;k+=4){q=a.substr(k,4);for(e=h=0;e<q.length;e+=1)d="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".indexOf(q[e]),h|=d<<18-6*e;for(e=0;e<q.length-1;e+=1){p=f+m;for(d=p>>>2;b.length<=d;)b.push(0);b[d]|=(h>>>16-8*e&255)<<8*(3+p%4*-1);f+=1}}return{value:b,binLen:8*f+c}};break;case "BYTES":d=function(a,b,c){var d,n,k,e,h;b=b||[0];c=c||0;k=c>>>3;for(n=0;n<a.length;n+=
1)d=a.charCodeAt(n),h=n+k,e=h>>>2,b.length<=e&&b.push(0),b[e]|=d<<8*(3+h%4*-1);return{value:b,binLen:8*a.length+c}};break;case "ARRAYBUFFER":try{d=new ArrayBuffer(0)}catch(l){throw Error("ARRAYBUFFER not supported by this environment");}d=function(a,b,c){var d,n,k,e,h;b=b||[0];c=c||0;n=c>>>3;h=new Uint8Array(a);for(d=0;d<a.byteLength;d+=1)e=d+n,k=e>>>2,b.length<=k&&b.push(0),b[k]|=h[d]<<8*(3+e%4*-1);return{value:b,binLen:8*a.byteLength+c}};break;default:throw Error("format must be HEX, TEXT, B64, BYTES, or ARRAYBUFFER");
}return d}function r(c,a){return c>>>a|c<<32-a}function J(c,a,d){return c&a^~c&d}function K(c,a,d){return c&a^c&d^a&d}function L(c){return r(c,2)^r(c,13)^r(c,22)}function M(c){return r(c,6)^r(c,11)^r(c,25)}function N(c){return r(c,7)^r(c,18)^c>>>3}function O(c){return r(c,17)^r(c,19)^c>>>10}function P(c,a){var d=(c&65535)+(a&65535);return((c>>>16)+(a>>>16)+(d>>>16)&65535)<<16|d&65535}function Q(c,a,d,l){var b=(c&65535)+(a&65535)+(d&65535)+(l&65535);return((c>>>16)+(a>>>16)+(d>>>16)+(l>>>16)+(b>>>
16)&65535)<<16|b&65535}function R(c,a,d,l,b){var g=(c&65535)+(a&65535)+(d&65535)+(l&65535)+(b&65535);return((c>>>16)+(a>>>16)+(d>>>16)+(l>>>16)+(b>>>16)+(g>>>16)&65535)<<16|g&65535}function x(c){var a=[],d;if(0===c.lastIndexOf("SHA-",0))switch(a=[3238371032,914150663,812702999,4144912697,4290775857,1750603025,1694076839,3204075428],d=[1779033703,3144134277,1013904242,2773480762,1359893119,2600822924,528734635,1541459225],c){case "SHA-224":break;case "SHA-256":a=d;break;case "SHA-384":a=[new m,new m,
new m,new m,new m,new m,new m,new m];break;case "SHA-512":a=[new m,new m,new m,new m,new m,new m,new m,new m];break;default:throw Error("Unknown SHA variant");}else throw Error("No SHA variants supported");return a}function A(c,a,d){var l,b,g,f,n,k,e,h,m,r,p,w,t,x,u,z,A,B,C,D,E,F,v=[],G;if("SHA-224"===d||"SHA-256"===d)r=64,w=1,F=Number,t=P,x=Q,u=R,z=N,A=O,B=L,C=M,E=K,D=J,G=H;else throw Error("Unexpected error in SHA-2 implementation");d=a[0];l=a[1];b=a[2];g=a[3];f=a[4];n=a[5];k=a[6];e=a[7];for(p=
0;p<r;p+=1)16>p?(m=p*w,h=c.length<=m?0:c[m],m=c.length<=m+1?0:c[m+1],v[p]=new F(h,m)):v[p]=x(A(v[p-2]),v[p-7],z(v[p-15]),v[p-16]),h=u(e,C(f),D(f,n,k),G[p],v[p]),m=t(B(d),E(d,l,b)),e=k,k=n,n=f,f=t(g,h),g=b,b=l,l=d,d=t(h,m);a[0]=t(d,a[0]);a[1]=t(l,a[1]);a[2]=t(b,a[2]);a[3]=t(g,a[3]);a[4]=t(f,a[4]);a[5]=t(n,a[5]);a[6]=t(k,a[6]);a[7]=t(e,a[7]);return a}var H;H=[1116352408,1899447441,3049323471,3921009573,961987163,1508970993,2453635748,2870763221,3624381080,310598401,607225278,1426881987,1925078388,2162078206,
2614888103,3248222580,3835390401,4022224774,264347078,604807628,770255983,1249150122,1555081692,1996064986,2554220882,2821834349,2952996808,3210313671,3336571891,3584528711,113926993,338241895,666307205,773529912,1294757372,1396182291,1695183700,1986661051,2177026350,2456956037,2730485921,2820302411,3259730800,3345764771,3516065817,3600352804,4094571909,275423344,430227734,506948616,659060556,883997877,958139571,1322822218,1537002063,1747873779,1955562222,2024104815,2227730452,2361852424,2428436474,
2756734187,3204031479,3329325298];"function"===typeof define&&define.amd?define(function(){return w}):"undefined"!==typeof exports?("undefined"!==typeof module&&module.exports&&(module.exports=w),exports=w):I.jsSHA=w})(this);

View file

@ -1,7 +1,5 @@
// +build !windows
/*
* Copyright (c) 2018 Unrud <unrud@outlook.com>
* Copyright (c) 2024 Unrud <unrud@outlook.com>
*
* This file is part of Remote-Touchpad.
*
@ -21,8 +19,24 @@
package main
import "errors"
import (
"fmt"
"io/fs"
"path/filepath"
"testing"
)
func InitWindowsBackend() (Backend, error) {
return nil, UnsupportedPlatformError{errors.New("disabled")}
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)
}
}