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 # 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) ## 1.0.3 (2021-09-18)
* Update dependencies * Update dependencies

View file

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

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> <launchable type="desktop-id">com.github.unrud.RemoteTouchpad.desktop</launchable>
<url type="bugtracker">https://github.com/Unrud/remote-touchpad/issues</url> <url type="bugtracker">https://github.com/Unrud/remote-touchpad/issues</url>
<url type="homepage">https://github.com/Unrud/remote-touchpad</url> <url type="homepage">https://github.com/Unrud/remote-touchpad</url>
<developer_name>Unrud</developer_name> <url type="vcs-browser">https://github.com/Unrud/remote-touchpad</url>
<developer_name translate="no">Unrud</developer_name>
<developer id="io.github.unrud">
<name translate="no">Unrud</name>
</developer>
<releases> <releases>
<release version="1.5.4" date="2026-08-02">
<description>
<ul>
<li>Improve X11 keyboard input</li>
<li>Update dependencies</li>
</ul>
</description>
</release>
<release version="1.5.3" date="2025-12-24">
<description>
<ul>
<li>Update dependencies</li>
</ul>
</description>
</release>
<release version="1.5.2" date="2025-10-12">
<description>
<ul>
<li>Update dependencies</li>
</ul>
</description>
</release>
<release version="1.5.0" date="2025-03-18">
<description>
<ul>
<li>Add restore token support for the remote desktop portal</li>
<li>Update dependencies</li>
</ul>
</description>
</release>
<release version="1.4.8" date="2024-09-01">
<description>
<ul>
<li>Update dependencies</li>
</ul>
</description>
</release>
<release version="1.4.7" date="2024-08-30">
<description>
<ul>
<li>Update dependencies</li>
</ul>
</description>
</release>
<release version="1.4.6" date="2024-02-06">
<description>
<ul>
<li>Prevent incorrect MIME types</li>
</ul>
</description>
</release>
<release version="1.4.5" date="2024-01-31">
<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"> <release version="1.0.3" date="2021-09-18">
<description> <description>
<ul> <ul>
@ -191,16 +373,16 @@
</releases> </releases>
<screenshots> <screenshots>
<screenshot type="default"> <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>
<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>
<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>
<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> </screenshot>
</screenshots> </screenshots>
<content_rating type="oars-1.1"/> <content_rating type="oars-1.1"/>

View file

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

View file

@ -1,14 +1,17 @@
# github.com/elazarl/go-bindata-assetfs v1.0.1 # github.com/bendahl/uinput v1.7.0
## explicit ## explicit; go 1.13
github.com/elazarl/go-bindata-assetfs github.com/bendahl/uinput
# github.com/godbus/dbus/v5 v5.0.5 # github.com/godbus/dbus/v5 v5.2.2
## explicit ## explicit; go 1.20
github.com/godbus/dbus/v5 github.com/godbus/dbus/v5
# github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e # 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
github.com/skip2/go-qrcode/bitset github.com/skip2/go-qrcode/bitset
github.com/skip2/go-qrcode/reedsolomon github.com/skip2/go-qrcode/reedsolomon
# golang.org/x/net v0.0.0-20210917221730-978cfadd31cf # golang.org/x/net v0.57.0
## explicit ## explicit; go 1.25.0
golang.org/x/net/websocket 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 module github.com/unrud/remote-touchpad
go 1.15 go 1.25.5
require ( require (
github.com/elazarl/go-bindata-assetfs v1.0.1 github.com/bendahl/uinput v1.7.0
github.com/godbus/dbus/v5 v5.0.5 github.com/godbus/dbus/v5 v5.2.2
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
golang.org/x/net v0.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/bendahl/uinput v1.7.0 h1:nA4fm8Wu8UYNOPykIZm66nkWEyvxzfmJ8YC02PM40jg=
github.com/elazarl/go-bindata-assetfs v1.0.1/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4= github.com/bendahl/uinput v1.7.0/go.mod h1:Np7w3DINc9wB83p12fTAM3DPPhFnAKP0WTXRqCQJ6Z8=
github.com/godbus/dbus/v5 v5.0.5 h1:9Eg0XUhQxtkV8ykTMKtMMYY72g4NgxtRq4jgh4Ih5YM= github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
github.com/godbus/dbus/v5 v5.0.5/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
golang.org/x/net v0.0.0-20210917221730-978cfadd31cf h1:R150MpwJIv1MpS0N/pc+NhTM8ajzvlmxlY5OYsrevXQ= golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
golang.org/x/net v0.0.0-20210917221730-978cfadd31cf/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
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=

10
host.go
View file

@ -28,7 +28,7 @@ import (
const ipv4Rating int = +1 const ipv4Rating int = +1
func FindDefaultHost() string { func findDefaultHost() string {
type hostsValue struct { type hostsValue struct {
prio int prio int
host string host string
@ -53,10 +53,9 @@ func FindDefaultHost() string {
panic(err) panic(err)
} }
ip := net.ParseIP(host) ip := net.ParseIP(host)
if ip == nil { if ip != nil {
panic("Invalid IP address: " + host) addIP(100, ip)
} }
addIP(100, ip)
} }
interfaces, _ := net.Interfaces() interfaces, _ := net.Interfaces()
for _, inter := range interfaces { for _, inter := range interfaces {
@ -75,7 +74,8 @@ func FindDefaultHost() string {
continue continue
} }
for _, linkLocalPrefix := range []string{ for _, linkLocalPrefix := range []string{
"169.254.", "fe8", "fe9", "fea", "feb"} { "169.254.", "fe8", "fe9", "fea", "feb",
} {
if strings.HasPrefix(ip.String(), linkLocalPrefix) { if strings.HasPrefix(ip.String(), linkLocalPrefix) {
addIP(20, ip) addIP(20, ip)
continue addrs continue addrs

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"?> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg <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" id="svg964"
version="1.1" version="1.1"
viewBox="0 0 264.58333 264.58334" viewBox="0 0 264.58333 264.58334"
height="1000" height="1000"
width="1000" width="1000"
sodipodi:docname="U+2630 Trigram for Heaven.svg" sodipodi:docname="U+2261 Identical To .svg"
inkscape:version="0.92.3 (2405546, 2018-03-11)"> 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 <sodipodi:namedview
pagecolor="#ffffff" pagecolor="#ffffff"
bordercolor="#666666" bordercolor="#666666"
@ -24,21 +24,22 @@
inkscape:pageopacity="0" inkscape:pageopacity="0"
inkscape:pageshadow="2" inkscape:pageshadow="2"
inkscape:window-width="1920" inkscape:window-width="1920"
inkscape:window-height="1016" inkscape:window-height="1011"
id="namedview8" id="namedview8"
showgrid="false" showgrid="false"
showguides="true" showguides="true"
inkscape:guide-bbox="true" inkscape:guide-bbox="true"
inkscape:zoom="1.1353809" inkscape:zoom="0.56769045"
inkscape:cx="173.83003" inkscape:cx="861.38493"
inkscape:cy="594.60287" inkscape:cy="500.27264"
inkscape:window-x="0" inkscape:window-x="0"
inkscape:window-y="27" inkscape:window-y="32"
inkscape:window-maximized="1" inkscape:window-maximized="1"
inkscape:current-layer="svg964" inkscape:current-layer="svg964"
inkscape:snap-global="true" inkscape:snap-global="true"
inkscape:measure-start="406.912,419.243" 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 <sodipodi:guide
position="0,52.916667" position="0,52.916667"
orientation="0,1" orientation="0,1"
@ -123,13 +124,13 @@
<path <path
inkscape:connector-curvature="0" 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" 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" 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="path825" id="path200"
sodipodi:nodetypes="ccccccccc" /> sodipodi:nodetypes="ccccccccc" />
<path <path
inkscape:connector-curvature="0" 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" 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" 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="path828" id="path202"
sodipodi:nodetypes="ccccccccc" /> sodipodi:nodetypes="ccccccccc" />
</svg> </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/>. * along with Remote-Touchpad. If not, see <http://www.gnu.org/licenses/>.
*/ */
package main package inputcontrol
type PointerButton int import "sort"
type Key int
type (
PointerButton int
Key int
)
const ( const (
PointerButtonLeft PointerButton = iota PointerButtonLeft PointerButton = iota
@ -39,34 +43,51 @@ const (
KeyBrowserBack KeyBrowserBack
KeyBrowserForward KeyBrowserForward
KeySuper KeySuper
KeyLeft
KeyRight
KeyUp
KeyDown
KeyHome
KeyEnd
KeyBackSpace
KeyDelete
KeyReturn
KeyLimit KeyLimit
) )
type BackendInfo struct { type ControllerInfo struct {
Name string Name string
Init func() (Backend, error) Init func() (Controller, error)
priority int
} }
var Backends []BackendInfo = []BackendInfo{ var Controllers []ControllerInfo
{"X11", InitX11Backend},
{"RemoteDesktop portal", InitPortalBackend}, func RegisterController(name string, init func() (Controller, error), priority int) {
{"Windows", InitWindowsBackend}, 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 { type UnsupportedPlatformError struct {
err error Err error
} }
func (e UnsupportedPlatformError) Error() string { func (e *UnsupportedPlatformError) Error() string {
return e.err.Error() return e.Err.Error()
} }
type Backend interface { func (e *UnsupportedPlatformError) Unwrap() error {
return e.Err
}
type Controller interface {
Close() error Close() error
KeyboardText(text string) error KeyboardText(text string) error
KeyboardKey(key Key) error KeyboardKey(key Key) error
PointerButton(button PointerButton, press bool) error PointerButton(button PointerButton, press bool) error
PointerMove(deltaX, deltaY int) error PointerMove(deltaX, deltaY int) error
PointerScroll(deltaHorizontal, deltaVertical int) error PointerScroll(deltaHorizontal, deltaVertical int, finish bool) error
PointerScrollFinish() 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> * Copyright (c) 2018 Unrud <unrud@outlook.com>
@ -19,10 +19,10 @@
* along with Remote-Touchpad. If not, see <http://www.gnu.org/licenses/>. * along with Remote-Touchpad. If not, see <http://www.gnu.org/licenses/>.
*/ */
package main package inputcontrol
import ( import (
"errors" "fmt"
"syscall" "syscall"
"unsafe" "unsafe"
) )
@ -34,6 +34,15 @@ const (
keyeventfKeyup uint32 = 0x2 keyeventfKeyup uint32 = 0x2
keyeventfUnicode uint32 = 0x4 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 vkLwin uint16 = 0x5B
vkBrowserBack uint16 = 0xA6 vkBrowserBack uint16 = 0xA6
vkBrowserForward uint16 = 0xA7 vkBrowserForward uint16 = 0xA7
@ -80,33 +89,36 @@ type keybdInput struct {
padding [8]byte padding [8]byte
} }
type windowsBackend struct{} type windowsController struct{}
func InitWindowsBackend() (Backend, error) { func init() {
p := &windowsBackend{} RegisterController("Windows", InitWindowsController, 0)
}
func InitWindowsController() (Controller, error) {
p := &windowsController{}
if err := sendInputProc.Find(); err != nil { if err := sendInputProc.Find(); err != nil {
return nil, UnsupportedPlatformError{err} return nil, &UnsupportedPlatformError{err}
} }
return p, nil return p, nil
} }
func (p *windowsBackend) Close() error { func (p *windowsController) Close() error {
return nil return nil
} }
func (p *windowsBackend) sendInput(inputs []keybdInput) error { func (p *windowsController) sendInput(inputs []keybdInput) error {
if len(inputs) == 0 { if len(inputs) == 0 {
return nil return nil
} }
if r, _, err := sendInputProc.Call(uintptr(len(inputs)), if sent, _, err := sendInputProc.Call(uintptr(len(inputs)),
uintptr(unsafe.Pointer(&inputs[0])), uintptr(unsafe.Pointer(&inputs[0])), unsafe.Sizeof(inputs[0])); int(sent) != len(inputs) {
unsafe.Sizeof(inputs[0])); int(r) != len(inputs) {
return err return err
} }
return nil return nil
} }
func (p *windowsBackend) KeyboardText(text string) error { func (p *windowsController) KeyboardText(text string) error {
inputs := make([]keybdInput, 0, len(text)*2) inputs := make([]keybdInput, 0, len(text)*2)
for _, runeValue := range text { for _, runeValue := range text {
in := keybdInput{typ: inputKeyboard, wScan: uint16(runeValue), dwFlags: keyeventfUnicode} in := keybdInput{typ: inputKeyboard, wScan: uint16(runeValue), dwFlags: keyeventfUnicode}
@ -120,35 +132,54 @@ func (p *windowsBackend) KeyboardText(text string) error {
return p.sendInput(inputs) return p.sendInput(inputs)
} }
func (p *windowsBackend) KeyboardKey(key Key) error { func (p *windowsController) KeyboardKey(key Key) error {
input := keybdInput{typ: inputKeyboard} 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 input.wVk = vkLwin
} else if key == KeyBrowserBack { case KeyBrowserBack:
input.wVk = vkBrowserBack input.wVk = vkBrowserBack
} else if key == KeyBrowserForward { case KeyBrowserForward:
input.wVk = vkBrowserForward input.wVk = vkBrowserForward
} else if key == KeyVolumeMute { case KeyVolumeMute:
input.wVk = vkVolumeMute input.wVk = vkVolumeMute
} else if key == KeyVolumeDown { case KeyVolumeDown:
input.wVk = vkVolumeDown input.wVk = vkVolumeDown
} else if key == KeyVolumeUp { case KeyVolumeUp:
input.wVk = vkVolumeUp input.wVk = vkVolumeUp
} else if key == KeyMediaNextTrack { case KeyMediaNextTrack:
input.wVk = vkMediaNextTrack input.wVk = vkMediaNextTrack
} else if key == KeyMediaPrevTrack { case KeyMediaPrevTrack:
input.wVk = vkMediaPrevTrack input.wVk = vkMediaPrevTrack
} else if key == KeyMediaPlayPause { case KeyMediaPlayPause:
input.wVk = vkMediaPlayPause input.wVk = vkMediaPlayPause
} else { default:
return errors.New("key not mapped to virtual-key code") return fmt.Errorf("key not mapped to virtual-key code: %#v", key)
} }
inputs := [...]keybdInput{input, input} inputs := [...]keybdInput{input, input}
inputs[1].dwFlags |= keyeventfKeyup inputs[1].dwFlags |= keyeventfKeyup
return p.sendInput(inputs[:]) 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} input := mouseInput{typ: inputMouse}
if button == PointerButtonLeft && press { if button == PointerButtonLeft && press {
input.dwFlags = mouseeventfLeftdown input.dwFlags = mouseeventfLeftdown
@ -163,30 +194,30 @@ func (p *windowsBackend) PointerButton(button PointerButton, press bool) error {
} else if button == PointerButtonRight { } else if button == PointerButtonRight {
input.dwFlags = mouseeventfRightup input.dwFlags = mouseeventfRightup
} else { } 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)), if sent, _, err := sendInputProc.Call(1, uintptr(unsafe.Pointer(&input)),
unsafe.Sizeof(input)); int(r) != 1 { unsafe.Sizeof(input)); int(sent) != 1 {
return err return err
} }
return nil return nil
} }
func (p *windowsBackend) PointerMove(deltaX, deltaY int) error { func (p *windowsController) PointerMove(deltaX, deltaY int) error {
input := mouseInput{ input := mouseInput{
typ: inputMouse, typ: inputMouse,
dx: int32(deltaX), dx: int32(deltaX),
dy: int32(deltaY), dy: int32(deltaY),
dwFlags: mouseeventfMove, dwFlags: mouseeventfMove,
} }
if r, _, err := sendInputProc.Call(1, uintptr(unsafe.Pointer(&input)), if sent, _, err := sendInputProc.Call(1, uintptr(unsafe.Pointer(&input)),
unsafe.Sizeof(input)); int(r) != 1 { unsafe.Sizeof(input)); int(sent) != 1 {
return err return err
} }
return nil 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) inputs := make([]mouseInput, 0, 2)
if deltaHorizontal != 0 { if deltaHorizontal != 0 {
inputs = append(inputs, mouseInput{ inputs = append(inputs, mouseInput{
@ -199,20 +230,15 @@ func (p *windowsBackend) PointerScroll(deltaHorizontal, deltaVertical int) error
inputs = append(inputs, mouseInput{ inputs = append(inputs, mouseInput{
typ: inputMouse, typ: inputMouse,
dwFlags: mouseeventfWheel, dwFlags: mouseeventfWheel,
mouseData: uint32(deltaVertical * scrollMult), mouseData: uint32(-deltaVertical * scrollMult),
}) })
} }
if len(inputs) == 0 { if len(inputs) == 0 {
return nil return nil
} }
if r, _, err := sendInputProc.Call(uintptr(len(inputs)), if sent, _, err := sendInputProc.Call(uintptr(len(inputs)),
uintptr(unsafe.Pointer(&inputs[0])), uintptr(unsafe.Pointer(&inputs[0])), unsafe.Sizeof(inputs[0])); int(sent) != len(inputs) {
unsafe.Sizeof(inputs[0])); int(r) != len(inputs) {
return err return err
} }
return nil 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> * Copyright (c) 2018 Unrud <unrud@outlook.com>
@ -19,52 +19,101 @@
* along with Remote-Touchpad. If not, see <http://www.gnu.org/licenses/>. * 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/Xlib.h>
// #include <X11/Intrinsic.h> // #include <X11/Intrinsic.h>
// #include <X11/extensions/Xrandr.h>
// #include <X11/extensions/XTest.h> // #include <X11/extensions/XTest.h>
// #include <X11/XKBlib.h> // #include <X11/XKBlib.h>
// Window MacroDefaultRootWindow(Display *dpy) {
// return DefaultRootWindow(dpy);
// }
import "C" import "C"
import ( import (
"errors" "errors"
"fmt" "fmt"
"os" "maps"
"slices"
"strings"
"sync" "sync"
"time" "time"
"unsafe" "unsafe"
) )
const ( const (
keyboardMappingDelay time.Duration = 500 * time.Millisecond keyboardMappingDelay time.Duration = 200 * time.Millisecond
scrollDiv int = 20 scrollDiv int = 20
) )
var modifierIndices [6]uint = [...]uint{C.ShiftMapIndex, C.Mod1MapIndex, var modifierIndexes = [...]int{
C.Mod2MapIndex, C.Mod3MapIndex, C.Mod4MapIndex, C.Mod5MapIndex} C.ShiftMapIndex,
C.Mod1MapIndex,
C.Mod2MapIndex,
C.Mod3MapIndex,
C.Mod4MapIndex,
C.Mod5MapIndex,
}
type x11Backend struct { type x11Controller struct {
display *C.Display display *C.Display
lock sync.Mutex lock sync.Mutex
scrollHorizontal, scrollVertical int scrollHorizontal, scrollVertical int
} }
func InitX11Backend() (Backend, error) { func init() {
sessionType := os.Getenv("XDG_SESSION_TYPE") RegisterController("X11", InitX11Controller, 0)
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 (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() p.lock.Lock()
defer p.lock.Unlock() defer p.lock.Unlock()
if p.display == nil { if p.display == nil {
@ -75,53 +124,68 @@ func (p *x11Backend) Close() error {
return nil 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 var minKeycodes, maxKeycodes C.int
C.XDisplayKeycodes(p.display, &minKeycodes, &maxKeycodes) C.XDisplayKeycodes(p.display, &minKeycodes, &maxKeycodes)
var keysymsPerKeycode C.int var keysymsPerKeycodeC C.int
keysyms := C.XGetKeyboardMapping(p.display, C.KeyCode(minKeycodes), keysyms := C.XGetKeyboardMapping(
maxKeycodes-minKeycodes+1, &keysymsPerKeycode) p.display,
C.KeyCode(minKeycodes),
maxKeycodes-minKeycodes+1,
&keysymsPerKeycodeC,
)
if keysyms == nil { if keysyms == nil {
return 0, 0, errors.New("failed to get keyboard mapping") return 0, 0, errors.New("failed to get keyboard mapping")
} }
defer C.XFree(unsafe.Pointer(keysyms)) defer C.XFree(unsafe.Pointer(keysyms))
keysymsPerKeycode = int(keysymsPerKeycodeC)
keycodes: keycodes:
for keycode := C.KeyCode(minKeycodes); keycode <= C.KeyCode(maxKeycodes); keycode++ { for keycode := int(minKeycodes); keycode <= int(maxKeycodes); keycode++ {
for i := 0; i < int(keysymsPerKeycode); i++ { for i := range keysymsPerKeycode {
keysymsIndex := int(keycode- keysymsIndex := (keycode-int(minKeycodes))*keysymsPerKeycode + i
C.KeyCode(minKeycodes))*int(keysymsPerKeycode) + i keysym := *(*C.KeySym)(unsafe.Pointer(
keysym := *(*C.KeySym)(unsafe.Pointer(uintptr(unsafe.Pointer(keysyms)) + uintptr(unsafe.Pointer(keysyms)) +
uintptr(keysymsIndex)*unsafe.Sizeof(*keysyms))) uintptr(keysymsIndex)*unsafe.Sizeof(*keysyms),
))
if keysym != 0 { if keysym != 0 {
continue keycodes continue keycodes
} }
} }
return keycode, keysymsPerKeycode, nil return C.KeyCode(keycode), keysymsPerKeycode, nil
} }
return 0, 0, errors.New("no empty keycode found") return 0, 0, errors.New("no empty keycode found")
} }
func (p *x11Backend) changeKeyMappingLocked(keysymsPerKeycode C.int, func (p *x11Controller) changeKeyMappingLocked(
keycode C.KeyCode, keysym Keysym) { keycode C.KeyCode, keysymsPerKeycode int, keysym Keysym,
) {
keycodeMapping := make([]C.KeySym, keysymsPerKeycode) keycodeMapping := make([]C.KeySym, keysymsPerKeycode)
for i := range keycodeMapping { for i := range keycodeMapping {
keycodeMapping[i] = C.KeySym(keysym) keycodeMapping[i] = C.KeySym(keysym)
} }
C.XChangeKeyboardMapping(p.display, C.int(keycode), keysymsPerKeycode, C.XChangeKeyboardMapping(
(*C.KeySym)(unsafe.Pointer(&keycodeMapping[0])), 1) p.display,
C.XFlush(p.display) C.int(keycode),
C.int(keysymsPerKeycode),
(*C.KeySym)(unsafe.Pointer(&keycodeMapping[0])),
1,
)
C.XSync(p.display, C.False)
} }
func (p *x11Backend) getModKeycodesLocked() map[uint]C.KeyCode { func (p *x11Controller) getModKeycodesLocked() map[uint]C.KeyCode {
modKeymap := C.XGetModifierMapping(p.display) modKeymap := C.XGetModifierMapping(p.display)
defer C.XFreeModifiermap(modKeymap) defer C.XFreeModifiermap(modKeymap)
modKeycodes := make(map[uint]C.KeyCode) modKeycodes := make(map[uint]C.KeyCode)
for _, modIndex := range modifierIndices { for _, modIndex := range modifierIndexes {
for i := 0; i < int(modKeymap.max_keypermod); i++ { for i := range int(modKeymap.max_keypermod) {
keycode := *(*C.KeyCode)(unsafe.Pointer(uintptr(unsafe.Pointer(modKeymap.modifiermap)) + keycodeIndex := modIndex*int(modKeymap.max_keypermod) + i
uintptr(uint(modIndex)*uint(modKeymap.max_keypermod)+uint(i)))) keycode := *(*C.KeyCode)(unsafe.Pointer(
uintptr(unsafe.Pointer(modKeymap.modifiermap)) +
uintptr(keycodeIndex)*unsafe.Sizeof(*modKeymap.modifiermap),
))
if keycode != 0 { if keycode != 0 {
modKeycodes[1<<uint(modIndex)] = keycode modKeycodes[1<<modIndex] = keycode
break break
} }
} }
@ -129,56 +193,45 @@ func (p *x11Backend) getModKeycodesLocked() map[uint]C.KeyCode {
return modKeycodes return modKeycodes
} }
func (p *x11Backend) findKeycodeLocked(keyboard C.XkbDescPtr, func (p *x11Controller) findKeycodeLocked(
modKeycodes map[uint]C.KeyCode, activeMods C.uint, keyboard C.XkbDescPtr, modKeycodes map[uint]C.KeyCode, activeMods uint, keysym Keysym,
keysym Keysym) (C.KeyCode, C.uint) { ) (keycode C.KeyCode, mods uint) {
keycode := C.XKeysymToKeycode(p.display, C.KeySym(keysym)) keycode = C.XKeysymToKeycode(p.display, C.KeySym(keysym))
if keycode == 0 { if keycode == 0 {
return 0, 0 return 0, 0
} }
var alwaysActiveMods C.uint for activeModBitmap := range uint(1) << len(modKeycodes) {
for modIndex := uint(0); modIndex < 8; modIndex++ { mods := activeMods
mod := uint(1) << modIndex for i, mod := range slices.Sorted(maps.Keys(modKeycodes)) {
if _, modAvailable := modKeycodes[mod]; !modAvailable { if activeModBitmap&(1<<i) != 0 {
alwaysActiveMods |= activeMods & C.uint(mod) mods ^= mod
}
}
_, shiftModAvailable := modKeycodes[C.ShiftMask]
for _, modIndex := range modifierIndices {
var mod C.uint
if modIndex != C.ShiftMapIndex {
mod = 1 << modIndex
}
for _, shiftMod := range [...]C.uint{0, C.ShiftMask} {
if shiftMod != 0 && !shiftModAvailable {
continue
}
mods := alwaysActiveMods | shiftMod | mod
var retMods C.uint
var retKeysym C.KeySym
C.XkbTranslateKeyCode(keyboard, keycode, mods, &retMods, &retKeysym)
if retKeysym == C.KeySym(keysym) {
return keycode, mods
} }
} }
var retMods C.uint
var retKeysym C.KeySym
C.XkbTranslateKeyCode(keyboard, keycode, C.uint(mods), &retMods, &retKeysym)
if retKeysym == C.KeySym(keysym) {
return keycode, mods
}
} }
return 0, 0 return 0, 0
} }
func (p *x11Backend) sendModsLocked(modKeycodes map[uint]C.KeyCode, mods C.uint, func (p *x11Controller) sendModsLocked(
press bool) { modKeycodes map[uint]C.KeyCode, mods uint, press bool,
) {
var pressC C.int = C.False var pressC C.int = C.False
if press { if press {
pressC = C.True pressC = C.True
} }
for mod, keycode := range modKeycodes { for mod, keycode := range modKeycodes {
if mods&C.uint(mod) != 0 { if mods&mod != 0 {
C.XTestFakeKeyEvent(p.display, C.uint(keycode), pressC, 0) C.XTestFakeKeyEvent(p.display, C.uint(keycode), pressC, 0)
} }
} }
} }
func (p *x11Backend) keyboardKeys(keys []Keysym) error { func (p *x11Controller) keyboardKeys(keys []Keysym) error {
p.lock.Lock() p.lock.Lock()
defer p.lock.Unlock() defer p.lock.Unlock()
if p.display == nil { if p.display == nil {
@ -193,17 +246,22 @@ func (p *x11Backend) keyboardKeys(keys []Keysym) error {
C.XkbCompatMapMask|C.XkbGeometryMask, C.XkbUseCoreKbd) C.XkbCompatMapMask|C.XkbGeometryMask, C.XkbUseCoreKbd)
defer C.XkbFreeKeyboard(keyboard, C.XkbAllComponentsMask, C.True) defer C.XkbFreeKeyboard(keyboard, C.XkbAllComponentsMask, C.True)
var emptyKeycode C.KeyCode var emptyKeycode C.KeyCode
var keysymsPerKeycode C.int var keysymsPerKeycode int
for _, keysym := range keys { for _, keysym := range keys {
var root, child C.Window var root, child C.Window
var rootX, rootY, x, y C.int var rootX, rootY, x, y C.int
var activeMods C.uint var activeModsC C.uint
C.XSync(p.display, C.False) C.XQueryPointer(
C.XQueryPointer(p.display, rootWindow, &root, &child, &rootX, &rootY, p.display,
&x, &y, &activeMods) rootWindow,
keycode, mods := p.findKeycodeLocked(keyboard, modKeycodes, activeMods, &root, &child,
keysym) &rootX, &rootY,
var pressMods, releaseMods C.uint &x, &y,
&activeModsC,
)
activeMods := uint(activeModsC)
keycode, mods := p.findKeycodeLocked(keyboard, modKeycodes, activeMods, keysym)
var pressMods, releaseMods uint
if keycode == 0 { if keycode == 0 {
if emptyKeycode == 0 { if emptyKeycode == 0 {
var err error var err error
@ -211,12 +269,10 @@ func (p *x11Backend) keyboardKeys(keys []Keysym) error {
if err != nil { if err != nil {
return err return err
} }
defer p.changeKeyMappingLocked(keysymsPerKeycode, emptyKeycode, 0) defer p.changeKeyMappingLocked(emptyKeycode, keysymsPerKeycode, 0)
} }
keycode = emptyKeycode keycode = emptyKeycode
p.changeKeyMappingLocked(keysymsPerKeycode, keycode, keysym) p.changeKeyMappingLocked(keycode, keysymsPerKeycode, keysym)
// race condition!
time.Sleep(keyboardMappingDelay)
} else { } else {
pressMods = mods & ^activeMods pressMods = mods & ^activeMods
releaseMods = activeMods & ^mods releaseMods = activeMods & ^mods
@ -227,7 +283,7 @@ func (p *x11Backend) keyboardKeys(keys []Keysym) error {
C.XTestFakeKeyEvent(p.display, C.uint(keycode), C.False, 0) C.XTestFakeKeyEvent(p.display, C.uint(keycode), C.False, 0)
p.sendModsLocked(modKeycodes, pressMods, false) p.sendModsLocked(modKeycodes, pressMods, false)
p.sendModsLocked(modKeycodes, releaseMods, true) p.sendModsLocked(modKeycodes, releaseMods, true)
C.XFlush(p.display) C.XSync(p.display, C.False)
if keycode == emptyKeycode { if keycode == emptyKeycode {
// race condition! // race condition!
time.Sleep(keyboardMappingDelay) time.Sleep(keyboardMappingDelay)
@ -236,7 +292,7 @@ func (p *x11Backend) keyboardKeys(keys []Keysym) error {
return nil return nil
} }
func (p *x11Backend) KeyboardText(text string) error { func (p *x11Controller) KeyboardText(text string) error {
keys := make([]Keysym, 0, len(text)) keys := make([]Keysym, 0, len(text))
for _, runeValue := range text { for _, runeValue := range text {
keysym, err := RuneToKeysym(runeValue) keysym, err := RuneToKeysym(runeValue)
@ -248,7 +304,7 @@ func (p *x11Backend) KeyboardText(text string) error {
return p.keyboardKeys(keys) return p.keyboardKeys(keys)
} }
func (p *x11Backend) KeyboardKey(key Key) error { func (p *x11Controller) KeyboardKey(key Key) error {
keysym, err := KeyToKeysym(key) keysym, err := KeyToKeysym(key)
if err != nil { if err != nil {
return err return err
@ -257,7 +313,7 @@ func (p *x11Backend) KeyboardKey(key Key) error {
return p.keyboardKeys(keys[:]) 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() p.lock.Lock()
defer p.lock.Unlock() defer p.lock.Unlock()
if p.display == nil { if p.display == nil {
@ -271,47 +327,52 @@ func (p *x11Backend) sendButton(button uint, press bool) error {
pressC = C.True pressC = C.True
} }
C.XTestFakeButtonEvent(p.display, C.uint(button), pressC, 0) C.XTestFakeButtonEvent(p.display, C.uint(button), pressC, 0)
C.XFlush(p.display) C.XSync(p.display, C.False)
return nil return nil
} }
func (p *x11Backend) PointerButton(button PointerButton, press bool) error { func (p *x11Controller) PointerButton(button PointerButton, press bool) error {
if button == PointerButtonLeft { switch button {
case PointerButtonLeft:
return p.sendButton(1, press) return p.sendButton(1, press)
} case PointerButtonRight:
if button == PointerButtonRight {
return p.sendButton(3, press) return p.sendButton(3, press)
} case PointerButtonMiddle:
if button == PointerButtonMiddle {
return p.sendButton(2, press) 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() p.lock.Lock()
defer p.lock.Unlock() defer p.lock.Unlock()
if p.display == nil { if p.display == nil {
return errors.New("X server connection closed") return errors.New("X server connection closed")
} }
C.XTestFakeRelativeMotionEvent(p.display, C.int(deltaX), C.int(deltaY), 0) C.XTestFakeRelativeMotionEvent(p.display, C.int(deltaX), C.int(deltaY), 0)
C.XFlush(p.display) C.XSync(p.display, C.False)
return nil return nil
} }
func (p *x11Backend) PointerScroll(deltaHorizontal, deltaVertical int) error { func (p *x11Controller) PointerScroll(deltaHorizontal, deltaVertical int, finish bool) error {
p.lock.Lock() p.lock.Lock()
stepsHorizontal := (p.scrollHorizontal + deltaHorizontal) / scrollDiv stepsHorizontal := (p.scrollHorizontal + deltaHorizontal) / scrollDiv
stepsVertical := (p.scrollVertical + deltaVertical) / scrollDiv stepsVertical := (p.scrollVertical + deltaVertical) / scrollDiv
p.scrollHorizontal = (p.scrollHorizontal + deltaHorizontal) % scrollDiv if finish {
p.scrollVertical = (p.scrollVertical + deltaVertical) % scrollDiv p.scrollHorizontal = 0
p.scrollVertical = 0
} else {
p.scrollHorizontal = (p.scrollHorizontal + deltaHorizontal) % scrollDiv
p.scrollVertical = (p.scrollVertical + deltaVertical) % scrollDiv
}
p.lock.Unlock() p.lock.Unlock()
var buttonHorizontal uint = 7 var buttonHorizontal uint = 7
if stepsHorizontal < 0 { if stepsHorizontal < 0 {
buttonHorizontal = 6 buttonHorizontal = 6
stepsHorizontal = -stepsHorizontal stepsHorizontal = -stepsHorizontal
} }
for i := 0; i < stepsHorizontal; i++ { for range stepsHorizontal {
if err := p.sendButton(buttonHorizontal, true); err != nil { if err := p.sendButton(buttonHorizontal, true); err != nil {
return err return err
} }
@ -324,7 +385,7 @@ func (p *x11Backend) PointerScroll(deltaHorizontal, deltaVertical int) error {
buttonVertical = 4 buttonVertical = 4
stepsVertical = -stepsVertical stepsVertical = -stepsVertical
} }
for i := 0; i < stepsVertical; i++ { for range stepsVertical {
if err := p.sendButton(buttonVertical, true); err != nil { if err := p.sendButton(buttonVertical, true); err != nil {
return err return err
} }
@ -334,11 +395,3 @@ func (p *x11Backend) PointerScroll(deltaHorizontal, deltaVertical int) error {
} }
return nil 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. // 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{ var keysymsMap = map[rune]Keysym{
0x0008: 0x0000ff08, 0x0008: 0x0000ff08,

View file

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

View file

@ -1,4 +1,4 @@
// +build portal x11 //go:build portal || x11
/* /*
* Copyright (c) 2018 Unrud <unrud@outlook.com> * Copyright (c) 2018 Unrud <unrud@outlook.com>
@ -19,17 +19,26 @@
* along with Remote-Touchpad. If not, see <http://www.gnu.org/licenses/>. * along with Remote-Touchpad. If not, see <http://www.gnu.org/licenses/>.
*/ */
package main package inputcontrol
//go:generate go run keysyms.generator.go //go:generate go run keysyms.generator.go
import "errors" import "fmt"
type Keysym int32 type Keysym int32
const ( const (
// X11/keysymdef.h // 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 // X11/XF86keysym.h
xf86xkAudioLowerVolume Keysym = 0x1008ff11 xf86xkAudioLowerVolume Keysym = 0x1008ff11
xf86xkAudioMute Keysym = 0x1008ff12 xf86xkAudioMute Keysym = 0x1008ff12
@ -48,8 +57,8 @@ func RuneToKeysym(runeValue rune) (Keysym, error) {
keysym, found := keysymsMap[runeValue] keysym, found := keysymsMap[runeValue]
if !found { if !found {
if runeValue < 0x100 || runeValue > 0x10ffff { if runeValue < 0x100 || runeValue > 0x10ffff {
return 0, errors.New("rune not mappend to keysym and " + return 0, fmt.Errorf("rune not mappend to keysym and "+
"out of range for direct unicode mapping") "out of range for direct unicode mapping: %q", runeValue)
} }
keysym = Keysym(0x01000000 + runeValue) keysym = Keysym(0x01000000 + runeValue)
} }
@ -57,32 +66,44 @@ func RuneToKeysym(runeValue rune) (Keysym, error) {
} }
func KeyToKeysym(key Key) (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 return xkSuperL, nil
} case KeyVolumeMute:
if key == KeyVolumeMute {
return xf86xkAudioMute, nil return xf86xkAudioMute, nil
} case KeyVolumeDown:
if key == KeyVolumeDown {
return xf86xkAudioLowerVolume, nil return xf86xkAudioLowerVolume, nil
} case KeyVolumeUp:
if key == KeyVolumeUp {
return xf86xkAudioRaiseVolume, nil return xf86xkAudioRaiseVolume, nil
} case KeyMediaPlayPause:
if key == KeyMediaPlayPause {
return xf86xkAudioPlay, nil return xf86xkAudioPlay, nil
} case KeyMediaPrevTrack:
if key == KeyMediaPrevTrack {
return xf86xkAudioPrev, nil return xf86xkAudioPrev, nil
} case KeyMediaNextTrack:
if key == KeyMediaNextTrack {
return xf86xkAudioNext, nil return xf86xkAudioNext, nil
} case KeyBrowserBack:
if key == KeyBrowserBack {
return xf86xkBack, nil return xf86xkBack, nil
} case KeyBrowserForward:
if key == KeyBrowserForward {
return xf86xkForward, nil 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" "errors"
"flag" "flag"
"fmt" "fmt"
"golang.org/x/net/websocket"
"log" "log"
mathrand "math/rand" mathrand "math/rand"
"net" "net"
@ -37,6 +36,10 @@ import (
"strings" "strings"
"time" "time"
"unicode/utf8" "unicode/utf8"
"github.com/unrud/remote-touchpad/inputcontrol"
"github.com/unrud/remote-touchpad/terminal"
"golang.org/x/net/websocket"
) )
const ( const (
@ -45,26 +48,31 @@ const (
authenticationRateBurst int = 10 authenticationRateBurst int = 10
challengeLength int = 8 challengeLength int = 8
defaultBind string = ":0" defaultBind string = ":0"
version string = "1.0.3" version string = "1.5.4"
pretty_app_name string = "Remote Touchpad" 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 { if len(command) == 0 {
return errors.New("empty command") return errors.New("empty command")
} }
if command == "sf" { if command == "S" {
return backend.PointerScrollFinish() return controller.PointerScroll(0, 0, true)
} }
if command[0] == 't' { if command[0] == 't' {
text := command[1:] 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) { if !utf8.ValidString(text) {
return errors.New("invalid utf-8") return errors.New("invalid utf-8")
} }
return backend.KeyboardText(text) return controller.KeyboardText(text)
} }
arguments := strings.Split(command[1:], ";") arguments := strings.Split(command[1:], ";")
if command[0] == 'k' && len(arguments) != 1 || if command[0] == 'k' && len(arguments) != 1 ||
@ -76,30 +84,33 @@ func processCommand(backend Backend, command string) error {
return err return err
} }
if command[0] == 'k' { if command[0] == 'k' {
if x < 0 || x >= int64(KeyLimit) { if x < 0 || x >= int64(inputcontrol.KeyLimit) {
return errors.New("unsupported key") return errors.New("unsupported key")
} }
return backend.KeyboardKey(Key(x)) return controller.KeyboardKey(inputcontrol.Key(x))
} }
y, err := strconv.ParseInt(arguments[1], 10, 32) y, err := strconv.ParseInt(arguments[1], 10, 32)
if err != nil { if err != nil {
return err return err
} }
if command[0] == 'm' { if command[0] == 'm' {
return backend.PointerMove(int(x), int(y)) return controller.PointerMove(int(x), int(y))
} }
if command[0] == 's' { 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 command[0] == 'b' {
if x < 0 || x >= int64(PointerButtonLimit) { if x < 0 || x >= int64(inputcontrol.PointerButtonLimit) {
return errors.New("unsupported pointer button") return errors.New("unsupported pointer button")
} }
b := true b := true
if y == 0 { if y == 0 {
b = false b = false
} }
return backend.PointerButton(PointerButton(x), b) return controller.PointerButton(inputcontrol.PointerButton(x), b)
} }
return errors.New("unsupported command") return errors.New("unsupported command")
} }
@ -140,14 +151,20 @@ func secureRandBase64(length int) string {
} }
func main() { func main() {
TerminalSetTitle(pretty_app_name) terminal.SetTitle(prettyAppName)
var bind, certFile, keyFile, secret string var bind, certFile, keyFile, secret string
var showVersion bool var showVersion bool
var config config
flag.BoolVar(&showVersion, "version", false, "show program's version number and exit") flag.BoolVar(&showVersion, "version", false, "show program's version number and exit")
flag.StringVar(&bind, "bind", defaultBind, "bind server to [HOSTNAME]:PORT") flag.StringVar(&bind, "bind", defaultBind, "bind server to [HOSTNAME]:PORT")
flag.StringVar(&secret, "secret", "", "shared secret for client authentication") flag.StringVar(&secret, "secret", "", "shared secret for client authentication")
flag.StringVar(&certFile, "cert", "", "file containing TLS certificate") flag.StringVar(&certFile, "cert", "", "file containing TLS certificate")
flag.StringVar(&keyFile, "key", "", "file containing TLS private key") 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() flag.Parse()
if showVersion { if showVersion {
fmt.Println(version) fmt.Println(version)
@ -163,25 +180,32 @@ func main() {
if secret == "" { if secret == "" {
secret = secureRandBase64(defaultSecretLength) secret = secureRandBase64(defaultSecretLength)
} }
var backend Backend if len(inputcontrol.Controllers) == 0 {
var backendName string log.Fatal("compiled without controller")
platformErrors := "" }
for _, backendInfo := range Backends { var controller inputcontrol.Controller
backendName = backendInfo.Name var controllerName string
var platformErrs []error
for _, controllerInfo := range inputcontrol.Controllers {
controllerName = controllerInfo.Name
var err error var err error
backend, err = backendInfo.Init() controller, err = controllerInfo.Init()
if err == nil { if err == nil {
break break
} else if _, ok := err.(UnsupportedPlatformError); ok {
platformErrors += fmt.Sprintf("%s backend: %v\n", backendName, err)
} else { } 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 { if controller == nil {
log.Fatal("unsupported platform:\n" + platformErrors) log.Fatal(fmt.Errorf("unsupported platform:\n%w", errors.Join(platformErrs...)))
} }
defer backend.Close() defer controller.Close()
authenticationChallenges := make(chan challenge, authenticationRateBurst) authenticationChallenges := make(chan challenge, authenticationRateBurst)
go authenticationChallengeGenerator(secret, authenticationChallenges) go authenticationChallengeGenerator(secret, authenticationChallenges)
listener, err := net.Listen("tcp", bind) listener, err := net.Listen("tcp", bind)
@ -201,11 +225,11 @@ func main() {
} }
} }
if host == "" { if host == "" {
host = FindDefaultHost() host = findDefaultHost()
} }
port := addr.Port port := addr.Port
mux := http.NewServeMux() mux := http.NewServeMux()
mux.Handle("/", http.FileServer(assetFS())) mux.Handle("/", http.FileServer(http.FS(webdataFS)))
mux.Handle("/ws", websocket.Handler(func(ws *websocket.Conn) { mux.Handle("/ws", websocket.Handler(func(ws *websocket.Conn) {
var message string var message string
challenge := <-authenticationChallenges challenge := <-authenticationChallenges
@ -216,12 +240,13 @@ func main() {
if !challenge.verify(message) { if !challenge.verify(message) {
return return
} }
websocket.JSON.Send(ws, config)
for { for {
if err := websocket.Message.Receive(ws, &message); err != nil { if err := websocket.Message.Receive(ws, &message); err != nil {
return return
} }
if err := processCommand(backend, message); err != nil { if err := processCommand(controller, message); err != nil {
log.Print(fmt.Sprintf("%s backend: %v", backendName, err)) log.Print(fmt.Errorf("%s controller: %w", controllerName, err))
return return
} }
} }
@ -234,16 +259,17 @@ func main() {
if tls { if tls {
scheme = "https" scheme = "https"
} }
url := fmt.Sprintf("%s://%s/#%s\n", scheme, domain, secret) url := fmt.Sprintf("%s://%s/#%s", scheme, domain, secret)
readyMsg := "ready: " + url fmt.Println(url)
qrCode, _ := GenerateQRCode(url, TerminalSupportsColor(os.Stderr.Fd())) if qrCode, err := terminal.GenerateQRCode(url, terminal.SupportsColor(os.Stdout.Fd())); err == nil {
readyMsg += qrCode fmt.Print(qrCode)
if !tls { } else {
readyMsg += ("" + log.Printf("QR code error: %v", err)
"▌ WARNING: TLS is not enabled ▐\n" + }
"▌Don't use in an untrusted network!▐\n") if !tls {
fmt.Println("▌ WARNING: TLS is not enabled ▐")
fmt.Println("▌Don't use in an untrusted network!▐")
} }
log.Print(readyMsg)
if tls { if tls {
err = http.ServeTLS(listener, mux, certFile, keyFile) err = http.ServeTLS(listener, mux, certFile, keyFile)
} else { } 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: # WARNING:
# Snapcraft uses caching for already build steps but it's buggy and can cause strange problems. # Snapcraft uses caching for already build steps but it's buggy and can cause strange problems.
# Clean the cache by running "snapcraft clean". # Clean the cache by running `snapcraft clean`.
name: remote-touchpad name: remote-touchpad
license: GPL-3.0+ license: GPL-3.0+
grade: stable grade: stable
adopt-info: remote-touchpad adopt-info: remote-touchpad
base: core18 base: core24
confinement: strict confinement: strict
platforms:
amd64:
arm64:
apps: apps:
remote-touchpad: remote-touchpad:
command: bin/remote-touchpad command: bin/remote-touchpad
@ -20,13 +24,13 @@ apps:
- desktop - desktop
- x11 - x11
remote-touchpad-wait-on-error: remote-touchpad-wait-on-error:
command: usr/bin/remote-touchpad-wait-on-error command: bin/remote-touchpad-wait-on-error
plugs: plugs:
- network-bind - network-bind
- desktop - desktop
- x11 - x11
common-id: com.github.unrud.RemoteTouchpad common-id: com.github.unrud.RemoteTouchpad
desktop: share/applications/com.github.unrud.RemoteTouchpad.desktop desktop: usr/share/applications/com.github.unrud.RemoteTouchpad.desktop
parts: parts:
remote-touchpad: remote-touchpad:
@ -36,18 +40,22 @@ parts:
- x11 - x11
source: . source: .
source-type: git source-type: git
build-snaps:
- go
build-packages: build-packages:
- gcc - gcc
- libc6-dev - libc6-dev
- libxt-dev - libxt-dev
- libxrandr-dev
- libxtst-dev - libxtst-dev
stage-packages: stage-packages:
- libxt6 - libxt6
- libxrandr2
- libxtst6 - libxtst6
override-build: | override-build: |
snapcraftctl build craftctl default
install -Dm0755 -t "${SNAPCRAFT_PART_INSTALL}/usr/bin" desktop/remote-touchpad-wait-on-error install -Dm0755 -t "${CRAFT_PART_INSTALL}/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 "${CRAFT_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 "${CRAFT_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 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] 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/>. * along with Remote-Touchpad. If not, see <http://www.gnu.org/licenses/>.
*/ */
package main package terminal
import qrcode "github.com/skip2/go-qrcode" import qrcode "github.com/skip2/go-qrcode"
@ -33,7 +33,7 @@ func qrCodeToString(bits [][]bool, colorize bool) string {
s := "" s := ""
for y := -1; y < len(bits); y += 2 { for y := -1; y < len(bits); y += 2 {
if colorize { if colorize {
s += TerminalForegroundWhite + TerminalBackgroundBlack s += ForegroundBlack + BackgroundWhite
} }
for x := range bits[0] { for x := range bits[0] {
upper := false upper := false
@ -45,17 +45,17 @@ func qrCodeToString(bits [][]bool, colorize bool) string {
lower = bits[y+1][x] lower = bits[y+1][x]
} }
if upper && lower { if upper && lower {
s += " " s += "█"
} else if !upper && lower {
s += "▀"
} else if upper && !lower { } else if upper && !lower {
s += "▀"
} else if !upper && lower {
s += "▄" s += "▄"
} else { } else {
s += "" s += " "
} }
} }
if colorize { if colorize {
s += TerminalForegroundReset + TerminalBackgroundReset s += ForegroundReset + BackgroundReset
} }
s += "\n" s += "\n"
} }

View file

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

View file

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

View file

@ -17,7 +17,7 @@
* along with Remote-Touchpad. If not, see <http://www.gnu.org/licenses/>. * along with Remote-Touchpad. If not, see <http://www.gnu.org/licenses/>.
*/ */
package main package terminal
import ( import (
"syscall" "syscall"
@ -36,13 +36,13 @@ var (
setConsoleTitleProc = kernel32DLL.NewProc("SetConsoleTitleW") setConsoleTitleProc = kernel32DLL.NewProc("SetConsoleTitleW")
) )
func TerminalSupportsColor(fd uintptr) bool { func SupportsColor(fd uintptr) bool {
r, _, _ := setConsoleModeProc.Call(fd, uintptr(enableProcessedOuput| r, _, _ := setConsoleModeProc.Call(fd, uintptr(enableProcessedOuput|
enableWrapAtEolOutput|enableVirtualTerminalProcessing)) enableWrapAtEolOutput|enableVirtualTerminalProcessing))
return r != 0 return r != 0
} }
func TerminalSetTitle(title string) bool { func SetTitle(title string) bool {
r, _, _ := setConsoleTitleProc.Call( r, _, _ := setConsoleTitleProc.Call(
uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(title)))) uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(title))))
return r != 0 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. * This file is part of Remote-Touchpad.
* *
@ -21,8 +19,34 @@
package main package main
import "errors" import (
"embed"
"io/fs"
"log"
"mime"
)
func InitPortalBackend() (Backend, error) { //go:embed webdata/*
return nil, UnsupportedPlatformError{errors.New("disabled")} 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> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no, interactive-widget=resizes-content">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=0"> <script src="app/main.mjs" type="module"></script>
<script src="fn.js"></script>
<script src="sha256.js"></script>
<title>Remote Touchpad</title> <title>Remote Touchpad</title>
<link href="main.css" media="screen" rel="stylesheet"> <link href="main.css" media="screen" rel="stylesheet">
<link href="icon.png" type="image/png" rel="icon"> <link href="icon.png" type="image/png" rel="icon">
<div id="opening"> <div id="opening" class="scene">
<noscript><p>JavaScript is required!</p></noscript> <noscript><p>JavaScript is required!</p></noscript>
<p>Connecting…</p> <p>Connecting…</p>
</div> </div>
<div id="closed" class="hidden">
<div id="closed" class="scene">
<p>Disconnected</p> <p>Disconnected</p>
<button id="reloadbutton"></button> <button class="large" onclick="location.reload()"></button>
</div> </div>
<div id="pad" class="hidden">
<p id="padlabel">Touchpad</p> <div id="pad" class="scene touch-input mouse-input keyboard-input allow-fullscreen">
<button id="keysbutton"></button> <p class="background">Touchpad</p>
<button id="fullscreenbutton"></button> <button class="top left" onclick="app.showKeys()"></button>
<button id="keyboardbutton"></button> <button class="bottom left visible-if-fullscreen-enabled" onclick="app.toggleFullscreen()"></button>
<button class="bottom right" onclick="app.showTextInput()"></button>
</div> </div>
<div id="keyboard" class="hidden">
<textarea id="text" cols=1 rows=1></textarea> <div id="text-input" class="scene">
<button id="sendbutton"></button> <textarea class="grow" cols=1 rows=1></textarea>
</div>
<div id="keys" class="hidden">
<div class="buttons"> <div class="buttons">
<button id="browserbackbutton"></button> <button onclick="app.showKeys('text-input', 1)"></button>
<button id="superbutton">🪟</button> <button class="grow" id="send-text" data-send-content="➣" data-back-content="⯇"></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>
</div> </div>
</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 { .hidden {
display: none; display: none !important;
} }
html { html {
@ -16,22 +16,27 @@ html {
background-color: #404040; background-color: #404040;
color: white; color: white;
background-image: url(tex.png); background-image: url(tex.png);
touch-action: none;
overflow: hidden;
} }
html, html,
body, body,
body > div { .scene {
width: 100%; width: 100%;
height: 100%; height: 100%;
margin: 0; margin: 0;
} }
body > div { .scene {
position: relative; position: relative;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
box-sizing: border-box; box-sizing: border-box;
align-items: center;
gap: 2rem;
padding: 0.5rem;
} }
p { p {
@ -41,40 +46,7 @@ p {
text-align: center; text-align: center;
} }
button { p.background {
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 {
color: gray; color: gray;
text-shadow: text-shadow:
-1px -1px 0 black, -1px -1px 0 black,
@ -84,43 +56,94 @@ button:focus {
opacity: 0.08; opacity: 0.08;
font-weight: bold; font-weight: bold;
font-size: 4rem; font-size: 4rem;
pointer-events: none;
} }
@media (max-width: 32rem) { @media (max-width: 32rem) {
#padlabel { p.background {
font-size: 13vw; 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; position: absolute;
max-width: 50%; max-width: 50%;
max-height: 50%; max-height: 50%;
} }
#fullscreenbutton { button.top {
left: 0;
bottom: 0;
border-bottom: none;
border-left: none;
}
#keyboardbutton {
right: 0;
bottom: 0;
border-right: none;
border-bottom: none;
}
#keysbutton {
left: 0;
top: 0; top: 0;
border-top: none; border-top: none;
}
button.bottom {
bottom: 0;
border-bottom: none;
}
button.left {
left: 0;
border-left: none; 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 { textarea {
flex-grow: 1;
border: none; border: none;
resize: none; resize: none;
font-size: 2rem; font-size: 2rem;
@ -130,49 +153,76 @@ textarea {
padding: 0 0.5rem; padding: 0 0.5rem;
} }
#sendbutton { textarea:focus {
border-bottom: none; outline: 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;
}
} }
#keys { #keys {
padding: 0.5rem; padding: 0;
padding-bottom: 0; justify-content: end;
justify-content: flex-end;
} }
#keys .buttons { #keys .page {
display: flex; display: grid;
justify-content: center; gap: 0.5rem;
margin-bottom: 0.5rem;
flex-shrink: 1;
height: 6rem;
min-height: 0; 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; border-right: none;
} }
#keys .buttons > button { #text-input .buttons > button:last-child {
flex-shrink: 1; border-bottom: none;
height: 100%;
width: 8rem;
min-width: 0;
} }
/* 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. * This file is part of Remote-Touchpad.
* *
@ -21,8 +19,24 @@
package main package main
import "errors" import (
"fmt"
"io/fs"
"path/filepath"
"testing"
)
func InitWindowsBackend() (Backend, error) { func TestWebdataTypesCompleteness(t *testing.T) {
return nil, UnsupportedPlatformError{errors.New("disabled")} 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)
}
} }