mirror of https://github.com/jetkvm/kvm.git
Compare commits
37 Commits
29241ec3a5
...
a4c15d5c7e
Author | SHA1 | Date |
---|---|---|
|
a4c15d5c7e | |
|
7240abaf3d | |
|
6dd65fbba6 | |
|
9698564550 | |
|
d0759150ee | |
|
a4d6da7085 | |
|
146cee9309 | |
|
9b3d1e0417 | |
|
707a33cb07 | |
|
f8f225df6a | |
|
e10f0db3ba | |
|
7065c42e91 | |
|
8364c37f9a | |
|
dd7b2d4dcf | |
|
5fb7a2117c | |
|
cd10112ff2 | |
|
f810f09ab0 | |
|
18c7b253ca | |
|
0bf05becb4 | |
|
12f0814f8c | |
|
219573e25c | |
|
e0be7edf96 | |
|
ab94eb1da4 | |
|
33a4f38702 | |
|
c90b0425c7 | |
|
a2771f0b91 | |
|
99a5e9d385 | |
|
0bef35e044 | |
|
22849fceab | |
|
b4dd4961fc | |
|
eeb103adf9 | |
|
8cf6b40dc3 | |
|
c6b05d4abe | |
|
51814dcc5e | |
|
5ba08de566 | |
|
3f320e50f7 | |
|
7a9fb7cbb1 |
|
@ -35,6 +35,9 @@ jobs:
|
|||
- name: Run tests
|
||||
run: |
|
||||
go test ./... -json > testreport.json
|
||||
- name: Make test cases
|
||||
run: |
|
||||
make build_dev_test
|
||||
- name: Golang Test Report
|
||||
uses: becheran/go-testreport@v0.3.2
|
||||
with:
|
||||
|
@ -43,4 +46,6 @@ jobs:
|
|||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: jetkvm-app
|
||||
path: bin/jetkvm_app
|
||||
path: |
|
||||
bin/jetkvm_app
|
||||
device-tests.tar.gz
|
|
@ -69,12 +69,54 @@ jobs:
|
|||
CI_USER: ${{ vars.JETKVM_CI_USER }}
|
||||
CI_HOST: ${{ vars.JETKVM_CI_HOST }}
|
||||
CI_SSH_PRIVATE: ${{ secrets.JETKVM_CI_SSH_PRIVATE }}
|
||||
- name: Run tests
|
||||
run: |
|
||||
set -e
|
||||
echo "+ Copying device-tests.tar.gz to remote host"
|
||||
ssh jkci "cat > /tmp/device-tests.tar.gz" < device-tests.tar.gz
|
||||
echo "+ Running go tests"
|
||||
ssh jkci ash << 'EOF'
|
||||
set -e
|
||||
TMP_DIR=$(mktemp -d)
|
||||
cd ${TMP_DIR}
|
||||
tar zxf /tmp/device-tests.tar.gz
|
||||
./gotestsum --format=testdox \
|
||||
--jsonfile=/tmp/device-tests.json \
|
||||
--post-run-command 'sh -c "echo $TESTS_FAILED > /tmp/device-tests.failed"' \
|
||||
--raw-command -- ./run_all_tests -json
|
||||
|
||||
GOTESTSUM_EXIT_CODE=$?
|
||||
if [ $GOTESTSUM_EXIT_CODE -ne 0 ]; then
|
||||
echo "❌ Tests failed (exit code: $GOTESTSUM_EXIT_CODE)"
|
||||
rm -rf ${TMP_DIR} /tmp/device-tests.tar.gz
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TESTS_FAILED=$(cat /tmp/device-tests.failed)
|
||||
if [ "$TESTS_FAILED" -ne 0 ]; then
|
||||
echo "❌ Tests failed $TESTS_FAILED tests failed"
|
||||
rm -rf ${TMP_DIR} /tmp/device-tests.tar.gz
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Tests passed"
|
||||
rm -rf ${TMP_DIR} /tmp/device-tests.tar.gz
|
||||
EOF
|
||||
ssh jkci "cat /tmp/device-tests.json" > device-tests.json
|
||||
- name: Set up Golang
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: "1.24.0"
|
||||
- name: Golang Test Report
|
||||
uses: becheran/go-testreport@v0.3.2
|
||||
with:
|
||||
input: "device-tests.json"
|
||||
- name: Deploy application
|
||||
run: |
|
||||
set -e
|
||||
# Copy the binary to the remote host
|
||||
echo "+ Copying the application to the remote host"
|
||||
cat jetkvm_app | gzip | ssh jkci "cat > /userdata/jetkvm/jetkvm_app.update.gz"
|
||||
cat bin/jetkvm_app | gzip | ssh jkci "cat > /userdata/jetkvm/jetkvm_app.update.gz"
|
||||
# Deploy and run the application on the remote host
|
||||
echo "+ Deploying the application on the remote host"
|
||||
ssh jkci ash <<EOF
|
||||
|
@ -108,15 +150,25 @@ jobs:
|
|||
run: |
|
||||
echo "+ Checking the status of the device"
|
||||
curl -v http://$CI_HOST/device/status && echo
|
||||
echo "+ Waiting for 10 seconds to allow all services to start"
|
||||
sleep 10
|
||||
echo "+ Waiting for 15 seconds to allow all services to start"
|
||||
sleep 15
|
||||
echo "+ Collecting logs"
|
||||
ssh jkci "cat /userdata/jetkvm/last.log" > last.log
|
||||
cat last.log
|
||||
local_log_tar=$(mktemp)
|
||||
ssh jkci ash > $local_log_tar <<'EOF'
|
||||
log_path=$(mktemp -d)
|
||||
dmesg > $log_path/dmesg.log
|
||||
cp /userdata/jetkvm/last.log $log_path/last.log
|
||||
tar -czf - -C $log_path .
|
||||
EOF
|
||||
tar -xf $local_log_tar
|
||||
cat dmesg.log last.log
|
||||
env:
|
||||
CI_HOST: ${{ vars.JETKVM_CI_HOST }}
|
||||
- name: Upload logs
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: device-logs
|
||||
path: last.log
|
||||
path: |
|
||||
last.log
|
||||
dmesg.log
|
||||
device-tests.json
|
||||
|
|
35
Makefile
35
Makefile
|
@ -15,6 +15,9 @@ GO_LDFLAGS := \
|
|||
-X $(PROMETHEUS_TAG).Revision=$(REVISION) \
|
||||
-X $(KVM_PKG_NAME).builtTimestamp=$(BUILDTS)
|
||||
|
||||
GO_CMD := GOOS=linux GOARCH=arm GOARM=7 go
|
||||
BIN_DIR := $(shell pwd)/bin
|
||||
|
||||
TEST_DIRS := $(shell find . -name "*_test.go" -type f -exec dirname {} \; | sort -u)
|
||||
|
||||
hash_resource:
|
||||
|
@ -22,31 +25,35 @@ hash_resource:
|
|||
|
||||
build_dev: hash_resource
|
||||
@echo "Building..."
|
||||
GOOS=linux GOARCH=arm GOARM=7 go build -ldflags="$(GO_LDFLAGS) -X $(KVM_PKG_NAME).builtAppVersion=$(VERSION_DEV)" -o bin/jetkvm_app cmd/main.go
|
||||
$(GO_CMD) build -ldflags="$(GO_LDFLAGS) -X $(KVM_PKG_NAME).builtAppVersion=$(VERSION_DEV)" -o $(BIN_DIR)/jetkvm_app cmd/main.go
|
||||
|
||||
build_test2json:
|
||||
GOOS=linux GOARCH=arm GOARM=7 go build -o bin/test2json cmd/test2json
|
||||
$(GO_CMD) build -o $(BIN_DIR)/test2json cmd/test2json
|
||||
|
||||
build_dev_test: build_test2json
|
||||
build_gotestsum:
|
||||
@echo "Building gotestsum..."
|
||||
$(GO_CMD) install gotest.tools/gotestsum@latest
|
||||
cp $(shell $(GO_CMD) env GOPATH)/bin/linux_arm/gotestsum $(BIN_DIR)/gotestsum
|
||||
|
||||
build_dev_test: build_test2json build_gotestsum
|
||||
# collect all directories that contain tests
|
||||
@echo "Building tests for devices ..."
|
||||
@rm -rf bin/tests && mkdir -p bin/tests
|
||||
@rm -rf $(BIN_DIR)/tests && mkdir -p $(BIN_DIR)/tests
|
||||
|
||||
@cat resource/dev_test.sh > bin/tests/run_all_tests
|
||||
@cat resource/dev_test.sh > $(BIN_DIR)/tests/run_all_tests
|
||||
@for test in $(TEST_DIRS); do \
|
||||
test_pkg_name=$$(echo $$test | sed 's/^.\///g'); \
|
||||
test_pkg_full_name=$(KVM_PKG_NAME)/$$(echo $$test | sed 's/^.\///g'); \
|
||||
test_filename=$$(echo $$test_pkg_name | sed 's/\//__/g')_test; \
|
||||
GOOS=linux GOARCH=arm GOARM=7 \
|
||||
go test -v \
|
||||
$(GO_CMD) test -v \
|
||||
-ldflags="$(GO_LDFLAGS) -X $(KVM_PKG_NAME).builtAppVersion=$(VERSION_DEV)" \
|
||||
-c -o bin/tests/$$test_filename $$test; \
|
||||
echo "runTest ./$$test_filename $$test_pkg_full_name" >> bin/tests/run_all_tests; \
|
||||
-c -o $(BIN_DIR)/tests/$$test_filename $$test; \
|
||||
echo "runTest ./$$test_filename $$test_pkg_full_name" >> $(BIN_DIR)/tests/run_all_tests; \
|
||||
done; \
|
||||
chmod +x bin/tests/run_all_tests; \
|
||||
cp bin/test2json bin/tests/; \
|
||||
chmod +x bin/tests/test2json; \
|
||||
tar czfv device-tests.tar.gz -C bin/tests .
|
||||
chmod +x $(BIN_DIR)/tests/run_all_tests; \
|
||||
cp $(BIN_DIR)/test2json $(BIN_DIR)/tests/ && chmod +x $(BIN_DIR)/tests/test2json; \
|
||||
cp $(BIN_DIR)/gotestsum $(BIN_DIR)/tests/ && chmod +x $(BIN_DIR)/tests/gotestsum; \
|
||||
tar czfv device-tests.tar.gz -C $(BIN_DIR)/tests .
|
||||
|
||||
frontend:
|
||||
cd ui && npm ci && npm run build:device
|
||||
|
@ -59,7 +66,7 @@ dev_release: frontend build_dev
|
|||
|
||||
build_release: frontend hash_resource
|
||||
@echo "Building release..."
|
||||
GOOS=linux GOARCH=arm GOARM=7 go build -ldflags="$(GO_LDFLAGS) -X $(KVM_PKG_NAME).builtAppVersion=$(VERSION)" -o bin/jetkvm_app cmd/main.go
|
||||
$(GO_CMD) build -ldflags="$(GO_LDFLAGS) -X $(KVM_PKG_NAME).builtAppVersion=$(VERSION)" -o bin/jetkvm_app cmd/main.go
|
||||
|
||||
release:
|
||||
@if rclone lsf r2://jetkvm-update/app/$(VERSION)/ | grep -q "jetkvm_app"; then \
|
||||
|
|
|
@ -87,6 +87,7 @@ type Config struct {
|
|||
LocalAuthMode string `json:"localAuthMode"` //TODO: fix it with migration
|
||||
WakeOnLanDevices []WakeOnLanDevice `json:"wake_on_lan_devices"`
|
||||
KeyboardMacros []KeyboardMacro `json:"keyboard_macros"`
|
||||
KeyboardLayout string `json:"keyboard_layout"`
|
||||
EdidString string `json:"hdmi_edid_string"`
|
||||
ActiveExtension string `json:"active_extension"`
|
||||
DisplayRotation string `json:"display_rotation"`
|
||||
|
@ -109,6 +110,7 @@ var defaultConfig = &Config{
|
|||
ActiveExtension: "",
|
||||
KeyboardMacros: []KeyboardMacro{},
|
||||
DisplayRotation: "270",
|
||||
KeyboardLayout: "en-US",
|
||||
DisplayMaxBrightness: 64,
|
||||
DisplayDimAfterSec: 120, // 2 minutes
|
||||
DisplayOffAfterSec: 1800, // 30 minutes
|
||||
|
|
|
@ -26,6 +26,7 @@ show_help() {
|
|||
echo "Optional:"
|
||||
echo " -u, --user <remote_user> Remote username (default: root)"
|
||||
echo " --run-go-tests Run go tests"
|
||||
echo " --run-go-tests-only Run go tests and exit"
|
||||
echo " --skip-ui-build Skip frontend/UI build"
|
||||
echo " --help Display this help message"
|
||||
echo
|
||||
|
@ -41,7 +42,8 @@ SKIP_UI_BUILD=false
|
|||
RESET_USB_HID_DEVICE=false
|
||||
LOG_TRACE_SCOPES="${LOG_TRACE_SCOPES:-jetkvm,cloud,websocket,native,jsonrpc}"
|
||||
RUN_GO_TESTS=false
|
||||
RUN_GO_TESTS_JSON=false
|
||||
RUN_GO_TESTS_ONLY=false
|
||||
|
||||
# Parse command line arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
|
@ -65,8 +67,9 @@ while [[ $# -gt 0 ]]; do
|
|||
RUN_GO_TESTS=true
|
||||
shift
|
||||
;;
|
||||
--run-go-tests-json)
|
||||
RUN_GO_TESTS_JSON=true
|
||||
--run-go-tests-only)
|
||||
RUN_GO_TESTS_ONLY=true
|
||||
RUN_GO_TESTS=true
|
||||
shift
|
||||
;;
|
||||
--help)
|
||||
|
@ -81,10 +84,6 @@ while [[ $# -gt 0 ]]; do
|
|||
esac
|
||||
done
|
||||
|
||||
if [ "$RUN_GO_TESTS_JSON" = true ]; then
|
||||
RUN_GO_TESTS=true
|
||||
fi
|
||||
|
||||
# Verify required parameters
|
||||
if [ -z "$REMOTE_HOST" ]; then
|
||||
msg_err "Error: Remote IP is a required parameter"
|
||||
|
@ -98,29 +97,51 @@ if [ "$SKIP_UI_BUILD" = false ]; then
|
|||
make frontend
|
||||
fi
|
||||
|
||||
msg_info "▶ Building go binary"
|
||||
make build_dev
|
||||
|
||||
if [ "$RUN_GO_TESTS" = true ]; then
|
||||
msg_info "▶ Building go tests"
|
||||
make build_dev_test
|
||||
|
||||
msg_info "▶ Copying device-tests.tar.gz to remote host"
|
||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" "cat > ${REMOTE_PATH}/device-tests.tar.gz" < device-tests.tar.gz
|
||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" "cat > /tmp/device-tests.tar.gz" < device-tests.tar.gz
|
||||
|
||||
msg_info "▶ Running go tests"
|
||||
TEST_ARGS=""
|
||||
if [ "$RUN_GO_TESTS_JSON" = true ]; then
|
||||
TEST_ARGS="-json"
|
||||
fi
|
||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" ash << EOF
|
||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" ash << 'EOF'
|
||||
set -e
|
||||
cd ${REMOTE_PATH}
|
||||
tar zxvf device-tests.tar.gz
|
||||
./run_all_tests $TEST_ARGS
|
||||
EOF
|
||||
TMP_DIR=$(mktemp -d)
|
||||
cd ${TMP_DIR}
|
||||
tar zxf /tmp/device-tests.tar.gz
|
||||
./gotestsum --format=testdox \
|
||||
--jsonfile=/tmp/device-tests.json \
|
||||
--post-run-command 'sh -c "echo $TESTS_FAILED > /tmp/device-tests.failed"' \
|
||||
--raw-command -- ./run_all_tests -json
|
||||
|
||||
GOTESTSUM_EXIT_CODE=$?
|
||||
if [ $GOTESTSUM_EXIT_CODE -ne 0 ]; then
|
||||
echo "❌ Tests failed (exit code: $GOTESTSUM_EXIT_CODE)"
|
||||
rm -rf ${TMP_DIR} /tmp/device-tests.tar.gz
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TESTS_FAILED=$(cat /tmp/device-tests.failed)
|
||||
if [ "$TESTS_FAILED" -ne 0 ]; then
|
||||
echo "❌ Tests failed $TESTS_FAILED tests failed"
|
||||
rm -rf ${TMP_DIR} /tmp/device-tests.tar.gz
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Tests passed"
|
||||
rm -rf ${TMP_DIR} /tmp/device-tests.tar.gz
|
||||
EOF
|
||||
|
||||
if [ "$RUN_GO_TESTS_ONLY" = true ]; then
|
||||
msg_info "▶ Go tests completed"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
msg_info "▶ Building go binary"
|
||||
make build_dev
|
||||
|
||||
# Kill any existing instances of the application
|
||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" "killall jetkvm_app_debug || true"
|
||||
|
||||
|
@ -128,6 +149,8 @@ ssh "${REMOTE_USER}@${REMOTE_HOST}" "killall jetkvm_app_debug || true"
|
|||
ssh "${REMOTE_USER}@${REMOTE_HOST}" "cat > ${REMOTE_PATH}/jetkvm_app_debug" < bin/jetkvm_app
|
||||
|
||||
if [ "$RESET_USB_HID_DEVICE" = true ]; then
|
||||
msg_info "▶ Resetting USB HID device"
|
||||
msg_warn "The option has been deprecated and will be removed in a future version, as JetKVM will now reset USB gadget configuration when needed"
|
||||
# Remove the old USB gadget configuration
|
||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" "rm -rf /sys/kernel/config/usb_gadget/jetkvm/configs/c.1/hid.usb*"
|
||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" "ls /sys/class/udc > /sys/kernel/config/usb_gadget/jetkvm/UDC"
|
||||
|
|
23
go.mod
23
go.mod
|
@ -1,6 +1,8 @@
|
|||
module github.com/jetkvm/kvm
|
||||
|
||||
go 1.23.0
|
||||
go 1.23.4
|
||||
|
||||
toolchain go1.24.3
|
||||
|
||||
require (
|
||||
github.com/Masterminds/semver/v3 v3.3.0
|
||||
|
@ -12,21 +14,25 @@ require (
|
|||
github.com/gin-contrib/logger v1.2.5
|
||||
github.com/gin-gonic/gin v1.10.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/guregu/null/v6 v6.0.0
|
||||
github.com/gwatts/rootcerts v0.0.0-20240401182218-3ab9db955caf
|
||||
github.com/hanwen/go-fuse/v2 v2.5.1
|
||||
github.com/hashicorp/go-envparse v0.1.0
|
||||
github.com/pion/logging v0.2.2
|
||||
github.com/pion/mdns/v2 v2.0.7
|
||||
github.com/pion/webrtc/v4 v4.0.0
|
||||
github.com/pojntfx/go-nbd v0.3.2
|
||||
github.com/prometheus/client_golang v1.21.0
|
||||
github.com/prometheus/common v0.62.0
|
||||
github.com/prometheus/procfs v0.15.1
|
||||
github.com/psanford/httpreadat v0.1.0
|
||||
github.com/rs/zerolog v1.34.0
|
||||
github.com/sourcegraph/tf-dag v0.2.2-0.20250131204052-3e8ff1477b4f
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/vishvananda/netlink v1.3.0
|
||||
go.bug.st/serial v1.6.2
|
||||
golang.org/x/crypto v0.36.0
|
||||
golang.org/x/net v0.38.0
|
||||
golang.org/x/crypto v0.38.0
|
||||
golang.org/x/net v0.40.0
|
||||
golang.org/x/sys v0.33.0
|
||||
)
|
||||
|
||||
replace github.com/pojntfx/go-nbd v0.3.2 => github.com/chemhack/go-nbd v0.0.0-20241006125820-59e45f5b1e7b
|
||||
|
@ -38,6 +44,7 @@ require (
|
|||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.5 // indirect
|
||||
github.com/creack/goselect v0.1.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||
github.com/gin-contrib/sse v1.0.0 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.0.2 // indirect
|
||||
|
@ -45,7 +52,7 @@ require (
|
|||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.26.0 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/guregu/null/v6 v6.0.0 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/compress v1.17.11 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
||||
|
@ -70,16 +77,16 @@ require (
|
|||
github.com/pion/stun/v3 v3.0.0 // indirect
|
||||
github.com/pion/transport/v3 v3.0.7 // indirect
|
||||
github.com/pion/turn/v4 v4.0.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/rogpeppe/go-internal v1.11.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
github.com/vishvananda/netns v0.0.4 // indirect
|
||||
github.com/wlynxg/anet v0.0.5 // indirect
|
||||
golang.org/x/arch v0.15.0 // indirect
|
||||
golang.org/x/oauth2 v0.24.0 // indirect
|
||||
golang.org/x/sys v0.32.0 // indirect
|
||||
golang.org/x/text v0.23.0 // indirect
|
||||
golang.org/x/text v0.25.0 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
|
28
go.sum
28
go.sum
|
@ -51,8 +51,8 @@ github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAu
|
|||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
|
@ -62,8 +62,6 @@ github.com/gwatts/rootcerts v0.0.0-20240401182218-3ab9db955caf h1:JO6ISZIvEUitto
|
|||
github.com/gwatts/rootcerts v0.0.0-20240401182218-3ab9db955caf/go.mod h1:5Kt9XkWvkGi2OHOq0QsGxebHmhCcqJ8KCbNg/a6+n+g=
|
||||
github.com/hanwen/go-fuse/v2 v2.5.1 h1:OQBE8zVemSocRxA4OaFJbjJ5hlpCmIWbGr7r0M4uoQQ=
|
||||
github.com/hanwen/go-fuse/v2 v2.5.1/go.mod h1:xKwi1cF7nXAOBCXujD5ie0ZKsxc8GGSA1rlMJc+8IJs=
|
||||
github.com/hashicorp/go-envparse v0.1.0 h1:bE++6bhIsNCPLvgDZkYqo3nA+/PFI51pkrHdmPSDFPY=
|
||||
github.com/hashicorp/go-envparse v0.1.0/go.mod h1:OHheN1GoygLlAkTlXLXvAdnXdZxy8JUweQ1rAXx1xnc=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
|
||||
|
@ -149,11 +147,13 @@ github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0leargg
|
|||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||
github.com/psanford/httpreadat v0.1.0 h1:VleW1HS2zO7/4c7c7zNl33fO6oYACSagjJIyMIwZLUE=
|
||||
github.com/psanford/httpreadat v0.1.0/go.mod h1:Zg7P+TlBm3bYbyHTKv/EdtSJZn3qwbPwpfZ/I9GKCRE=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||
github.com/sourcegraph/tf-dag v0.2.2-0.20250131204052-3e8ff1477b4f h1:VgoRCP1efSCEZIcF2THLQ46+pIBzzgNiaUBe9wEDwYU=
|
||||
github.com/sourcegraph/tf-dag v0.2.2-0.20250131204052-3e8ff1477b4f/go.mod h1:pzro7BGorij2WgrjEammtrkbo3+xldxo+KaGLGUiD+Q=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
|
@ -181,10 +181,10 @@ go.bug.st/serial v1.6.2 h1:kn9LRX3sdm+WxWKufMlIRndwGfPWsH1/9lCWXQCasq8=
|
|||
go.bug.st/serial v1.6.2/go.mod h1:UABfsluHAiaNI+La2iESysd9Vetq7VRdpxvjx7CmmOE=
|
||||
golang.org/x/arch v0.15.0 h1:QtOrQd0bTUnhNVNndMpLHNWrDmYzZ2KDqSrEymqInZw=
|
||||
golang.org/x/arch v0.15.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE=
|
||||
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
||||
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
||||
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
||||
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
|
||||
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
|
||||
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
||||
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
|
||||
golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
|
||||
golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
|
@ -194,10 +194,10 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
||||
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
|
|
@ -0,0 +1,436 @@
|
|||
package usbgadget
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/procfs"
|
||||
"github.com/sourcegraph/tf-dag/dag"
|
||||
)
|
||||
|
||||
// it's a minimalistic implementation of ansible's file module with some modifications
|
||||
// to make it more suitable for our use case
|
||||
// https://docs.ansible.com/ansible/latest/modules/file_module.html
|
||||
|
||||
// we use this to check if the files in the gadget config are in the expected state
|
||||
// and to update them if they are not in the expected state
|
||||
|
||||
type FileState uint8
|
||||
type ChangeState uint8
|
||||
type FileChangeResolvedAction uint8
|
||||
|
||||
type ApplyFunc func(c *ChangeSet, changes []*FileChange) error
|
||||
|
||||
const (
|
||||
FileStateUnknown FileState = iota
|
||||
FileStateAbsent
|
||||
FileStateDirectory
|
||||
FileStateFile
|
||||
FileStateFileContentMatch
|
||||
FileStateFileWrite // update file content without checking
|
||||
FileStateMounted
|
||||
FileStateMountedConfigFS
|
||||
FileStateSymlink
|
||||
FileStateSymlinkInOrderConfigFS // configfs is a shithole, so we need to check if the symlinks are created in the correct order
|
||||
FileStateSymlinkNotInOrderConfigFS
|
||||
FileStateTouch
|
||||
)
|
||||
|
||||
var FileStateString = map[FileState]string{
|
||||
FileStateUnknown: "UNKNOWN",
|
||||
FileStateAbsent: "ABSENT",
|
||||
FileStateDirectory: "DIRECTORY",
|
||||
FileStateFile: "FILE",
|
||||
FileStateFileContentMatch: "FILE_CONTENT_MATCH",
|
||||
FileStateFileWrite: "FILE_WRITE",
|
||||
FileStateMounted: "MOUNTED",
|
||||
FileStateMountedConfigFS: "CONFIGFS_MOUNTED",
|
||||
FileStateSymlink: "SYMLINK",
|
||||
FileStateSymlinkInOrderConfigFS: "SYMLINK_IN_ORDER_CONFIGFS",
|
||||
FileStateTouch: "TOUCH",
|
||||
}
|
||||
|
||||
const (
|
||||
ChangeStateUnknown ChangeState = iota
|
||||
ChangeStateRequired
|
||||
ChangeStateNotChanged
|
||||
ChangeStateChanged
|
||||
ChangeStateError
|
||||
)
|
||||
|
||||
const (
|
||||
FileChangeResolvedActionUnknown FileChangeResolvedAction = iota
|
||||
FileChangeResolvedActionDoNothing
|
||||
FileChangeResolvedActionRemove
|
||||
FileChangeResolvedActionCreateFile
|
||||
FileChangeResolvedActionWriteFile
|
||||
FileChangeResolvedActionUpdateFile
|
||||
FileChangeResolvedActionAppendFile
|
||||
FileChangeResolvedActionCreateSymlink
|
||||
FileChangeResolvedActionRecreateSymlink
|
||||
FileChangeResolvedActionCreateDirectoryAndSymlinks
|
||||
FileChangeResolvedActionReorderSymlinks
|
||||
FileChangeResolvedActionCreateDirectory
|
||||
FileChangeResolvedActionRemoveDirectory
|
||||
FileChangeResolvedActionTouch
|
||||
FileChangeResolvedActionMountConfigFS
|
||||
)
|
||||
|
||||
var FileChangeResolvedActionString = map[FileChangeResolvedAction]string{
|
||||
FileChangeResolvedActionUnknown: "UNKNOWN",
|
||||
FileChangeResolvedActionDoNothing: "DO_NOTHING",
|
||||
FileChangeResolvedActionRemove: "REMOVE",
|
||||
FileChangeResolvedActionCreateFile: "FILE_CREATE",
|
||||
FileChangeResolvedActionWriteFile: "FILE_WRITE",
|
||||
FileChangeResolvedActionUpdateFile: "FILE_UPDATE",
|
||||
FileChangeResolvedActionAppendFile: "FILE_APPEND",
|
||||
FileChangeResolvedActionCreateSymlink: "SYMLINK_CREATE",
|
||||
FileChangeResolvedActionRecreateSymlink: "SYMLINK_RECREATE",
|
||||
FileChangeResolvedActionCreateDirectoryAndSymlinks: "DIR_CREATE_AND_SYMLINKS",
|
||||
FileChangeResolvedActionReorderSymlinks: "SYMLINK_REORDER",
|
||||
FileChangeResolvedActionCreateDirectory: "DIR_CREATE",
|
||||
FileChangeResolvedActionRemoveDirectory: "DIR_REMOVE",
|
||||
FileChangeResolvedActionTouch: "TOUCH",
|
||||
FileChangeResolvedActionMountConfigFS: "CONFIGFS_MOUNT",
|
||||
}
|
||||
|
||||
type ChangeSet struct {
|
||||
Changes []FileChange
|
||||
}
|
||||
|
||||
type RequestedFileChange struct {
|
||||
Component string
|
||||
Key string
|
||||
Path string // will be used as Key if Key is empty
|
||||
ParamSymlinks []symlink
|
||||
ExpectedState FileState
|
||||
ExpectedContent []byte
|
||||
DependsOn []string
|
||||
BeforeChange []string // if the file is going to be changed, apply the change first
|
||||
Description string
|
||||
IgnoreErrors bool
|
||||
When string // only apply the change if when meets the condition
|
||||
}
|
||||
|
||||
type FileChange struct {
|
||||
RequestedFileChange
|
||||
ActualState FileState
|
||||
ActualContent []byte
|
||||
resolvedDeps []string
|
||||
checked bool
|
||||
changed ChangeState
|
||||
action FileChangeResolvedAction
|
||||
}
|
||||
|
||||
func (f *RequestedFileChange) String() string {
|
||||
var s string
|
||||
switch f.ExpectedState {
|
||||
case FileStateDirectory:
|
||||
s = fmt.Sprintf("dir: %s", f.Path)
|
||||
case FileStateFile:
|
||||
s = fmt.Sprintf("file: %s", f.Path)
|
||||
case FileStateSymlink:
|
||||
s = fmt.Sprintf("symlink: %s -> %s", f.Path, f.ExpectedContent)
|
||||
case FileStateSymlinkInOrderConfigFS:
|
||||
s = fmt.Sprintf("symlink_in_order_configfs: %s -> %s", f.Path, f.ExpectedContent)
|
||||
case FileStateSymlinkNotInOrderConfigFS:
|
||||
s = fmt.Sprintf("symlink_not_in_order_configfs: %s -> %s", f.Path, f.ExpectedContent)
|
||||
case FileStateAbsent:
|
||||
s = fmt.Sprintf("absent: %s", f.Path)
|
||||
case FileStateFileContentMatch:
|
||||
s = fmt.Sprintf("file: %s with content [%s]", f.Path, f.ExpectedContent)
|
||||
case FileStateFileWrite:
|
||||
s = fmt.Sprintf("write: %s with content [%s]", f.Path, f.ExpectedContent)
|
||||
case FileStateMountedConfigFS:
|
||||
s = fmt.Sprintf("configfs: %s", f.Path)
|
||||
case FileStateTouch:
|
||||
s = fmt.Sprintf("touch: %s", f.Path)
|
||||
case FileStateUnknown:
|
||||
s = fmt.Sprintf("unknown change for %s", f.Path)
|
||||
default:
|
||||
s = fmt.Sprintf("unknown expected state %d for %s", f.ExpectedState, f.Path)
|
||||
}
|
||||
|
||||
if len(f.Description) > 0 {
|
||||
s += fmt.Sprintf(" (%s)", f.Description)
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (f *RequestedFileChange) IsSame(other *RequestedFileChange) bool {
|
||||
return f.Path == other.Path &&
|
||||
f.ExpectedState == other.ExpectedState &&
|
||||
reflect.DeepEqual(f.ExpectedContent, other.ExpectedContent) &&
|
||||
reflect.DeepEqual(f.DependsOn, other.DependsOn) &&
|
||||
f.IgnoreErrors == other.IgnoreErrors
|
||||
}
|
||||
|
||||
func (fc *FileChange) checkIfDirIsMountPoint() error {
|
||||
// check if the file is a mount point
|
||||
mounts, err := procfs.GetMounts()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get mounts")
|
||||
}
|
||||
|
||||
for _, mount := range mounts {
|
||||
if mount.MountPoint == fc.Path {
|
||||
fc.ActualState = FileStateMounted
|
||||
fc.ActualContent = []byte(mount.Source)
|
||||
|
||||
if mount.FSType == "configfs" {
|
||||
fc.ActualState = FileStateMountedConfigFS
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetActualState returns the actual state of the file at the given path.
|
||||
func (fc *FileChange) getActualState() error {
|
||||
l := defaultLogger.With().Str("path", fc.Path).Logger()
|
||||
|
||||
fi, err := os.Lstat(fc.Path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
fc.ActualState = FileStateAbsent
|
||||
} else {
|
||||
l.Warn().Err(err).Msg("failed to stat file")
|
||||
fc.ActualState = FileStateUnknown
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// check if the file is a symlink
|
||||
if fi.Mode()&os.ModeSymlink == os.ModeSymlink {
|
||||
fc.ActualState = FileStateSymlink
|
||||
// get the target of the symlink
|
||||
target, err := os.Readlink(fc.Path)
|
||||
if err != nil {
|
||||
l.Warn().Err(err).Msg("failed to read symlink")
|
||||
return fmt.Errorf("failed to read symlink")
|
||||
}
|
||||
// check if the target is a relative path
|
||||
if !filepath.IsAbs(target) {
|
||||
// make it absolute
|
||||
target, err = filepath.Abs(filepath.Join(filepath.Dir(fc.Path), target))
|
||||
if err != nil {
|
||||
l.Warn().Err(err).Msg("failed to make symlink target absolute")
|
||||
return fmt.Errorf("failed to make symlink target absolute")
|
||||
}
|
||||
}
|
||||
fc.ActualContent = []byte(target)
|
||||
return nil
|
||||
}
|
||||
|
||||
if fi.IsDir() {
|
||||
fc.ActualState = FileStateDirectory
|
||||
|
||||
switch fc.ExpectedState {
|
||||
case FileStateMountedConfigFS:
|
||||
err := fc.checkIfDirIsMountPoint()
|
||||
if err != nil {
|
||||
l.Warn().Err(err).Msg("failed to check if dir is mount point")
|
||||
return err
|
||||
}
|
||||
case FileStateSymlinkInOrderConfigFS:
|
||||
state, err := checkIfSymlinksInOrder(fc, &l)
|
||||
if err != nil {
|
||||
l.Warn().Err(err).Msg("failed to check if symlinks are in order")
|
||||
return err
|
||||
}
|
||||
fc.ActualState = state
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if fi.Mode()&os.ModeDevice == os.ModeDevice {
|
||||
l.Info().Msg("file is a device")
|
||||
return nil
|
||||
}
|
||||
|
||||
// check if the file is a regular file
|
||||
if fi.Mode().IsRegular() {
|
||||
fc.ActualState = FileStateFile
|
||||
// get the content of the file
|
||||
content, err := os.ReadFile(fc.Path)
|
||||
if err != nil {
|
||||
l.Warn().Err(err).Msg("failed to read file")
|
||||
return fmt.Errorf("failed to read file")
|
||||
}
|
||||
fc.ActualContent = content
|
||||
return nil
|
||||
}
|
||||
|
||||
l.Warn().Interface("file_info", fi.Mode()).Bool("is_dir", fi.IsDir()).Msg("unknown file type")
|
||||
|
||||
return fmt.Errorf("unknown file type")
|
||||
}
|
||||
|
||||
func (fc *FileChange) ResetActionResolution() {
|
||||
fc.checked = false
|
||||
fc.action = FileChangeResolvedActionUnknown
|
||||
fc.changed = ChangeStateUnknown
|
||||
}
|
||||
|
||||
func (fc *FileChange) Action() FileChangeResolvedAction {
|
||||
if !fc.checked {
|
||||
fc.action = fc.getFileChangeResolvedAction()
|
||||
fc.checked = true
|
||||
}
|
||||
|
||||
return fc.action
|
||||
}
|
||||
|
||||
func (fc *FileChange) getFileChangeResolvedAction() FileChangeResolvedAction {
|
||||
l := defaultLogger.With().Str("path", fc.Path).Logger()
|
||||
|
||||
// some actions are not needed to be checked
|
||||
switch fc.ExpectedState {
|
||||
case FileStateFileWrite:
|
||||
return FileChangeResolvedActionWriteFile
|
||||
case FileStateTouch:
|
||||
return FileChangeResolvedActionTouch
|
||||
}
|
||||
|
||||
// get the actual state of the file
|
||||
err := fc.getActualState()
|
||||
if err != nil {
|
||||
return FileChangeResolvedActionDoNothing
|
||||
}
|
||||
|
||||
baseName := filepath.Base(fc.Path)
|
||||
|
||||
switch fc.ExpectedState {
|
||||
case FileStateDirectory:
|
||||
// if the file is already a directory, do nothing
|
||||
if fc.ActualState == FileStateDirectory {
|
||||
return FileChangeResolvedActionDoNothing
|
||||
}
|
||||
return FileChangeResolvedActionCreateDirectory
|
||||
case FileStateFile:
|
||||
// if the file is already a file, do nothing
|
||||
if fc.ActualState == FileStateFile {
|
||||
return FileChangeResolvedActionDoNothing
|
||||
}
|
||||
return FileChangeResolvedActionCreateFile
|
||||
case FileStateFileContentMatch:
|
||||
// if the file is already a file with the expected content, do nothing
|
||||
if fc.ActualState == FileStateFile {
|
||||
looserMatch := baseName == "inquiry_string"
|
||||
if compareFileContent(fc.ActualContent, fc.ExpectedContent, looserMatch) {
|
||||
return FileChangeResolvedActionDoNothing
|
||||
}
|
||||
// TODO: move this to somewhere else
|
||||
// this is a workaround for the fact that the file is not updated if it has no content
|
||||
if baseName == "file" &&
|
||||
bytes.Equal(fc.ActualContent, []byte{}) &&
|
||||
bytes.Equal(fc.ExpectedContent, []byte{0x0a}) {
|
||||
return FileChangeResolvedActionDoNothing
|
||||
}
|
||||
return FileChangeResolvedActionUpdateFile
|
||||
}
|
||||
return FileChangeResolvedActionCreateFile
|
||||
case FileStateSymlink:
|
||||
// if the file is already a symlink, check if the target is the same
|
||||
if fc.ActualState == FileStateSymlink {
|
||||
if reflect.DeepEqual(fc.ActualContent, fc.ExpectedContent) {
|
||||
return FileChangeResolvedActionDoNothing
|
||||
}
|
||||
return FileChangeResolvedActionRecreateSymlink
|
||||
}
|
||||
return FileChangeResolvedActionCreateSymlink
|
||||
case FileStateSymlinkInOrderConfigFS:
|
||||
// if the file is already a symlink, check if the target is the same
|
||||
if fc.ActualState == FileStateSymlinkInOrderConfigFS {
|
||||
return FileChangeResolvedActionDoNothing
|
||||
}
|
||||
return FileChangeResolvedActionReorderSymlinks
|
||||
case FileStateAbsent:
|
||||
if fc.ActualState == FileStateAbsent {
|
||||
return FileChangeResolvedActionDoNothing
|
||||
}
|
||||
return FileChangeResolvedActionRemove
|
||||
case FileStateMountedConfigFS:
|
||||
if fc.ActualState == FileStateMountedConfigFS {
|
||||
return FileChangeResolvedActionDoNothing
|
||||
}
|
||||
return FileChangeResolvedActionMountConfigFS
|
||||
default:
|
||||
l.Warn().Interface("file_change", FileStateString[fc.ExpectedState]).Msg("unknown expected state")
|
||||
return FileChangeResolvedActionDoNothing
|
||||
}
|
||||
}
|
||||
|
||||
func (c *ChangeSet) AddFileChangeStruct(r RequestedFileChange) {
|
||||
fc := FileChange{
|
||||
RequestedFileChange: r,
|
||||
}
|
||||
c.Changes = append(c.Changes, fc)
|
||||
}
|
||||
|
||||
func (c *ChangeSet) AddFileChange(component string, path string, expectedState FileState, expectedContent []byte, dependsOn []string, description string) {
|
||||
c.AddFileChangeStruct(RequestedFileChange{
|
||||
Component: component,
|
||||
Path: path,
|
||||
ExpectedState: expectedState,
|
||||
ExpectedContent: expectedContent,
|
||||
DependsOn: dependsOn,
|
||||
Description: description,
|
||||
})
|
||||
}
|
||||
|
||||
func (c *ChangeSet) ApplyChanges() error {
|
||||
r := ChangeSetResolver{
|
||||
changeset: c,
|
||||
g: &dag.AcyclicGraph{},
|
||||
l: defaultLogger,
|
||||
}
|
||||
|
||||
return r.Apply()
|
||||
}
|
||||
|
||||
func (c *ChangeSet) applyChange(change *FileChange) error {
|
||||
switch change.Action() {
|
||||
case FileChangeResolvedActionWriteFile:
|
||||
return os.WriteFile(change.Path, change.ExpectedContent, 0644)
|
||||
case FileChangeResolvedActionUpdateFile:
|
||||
return os.WriteFile(change.Path, change.ExpectedContent, 0644)
|
||||
case FileChangeResolvedActionCreateFile:
|
||||
return os.WriteFile(change.Path, change.ExpectedContent, 0644)
|
||||
case FileChangeResolvedActionCreateSymlink:
|
||||
return os.Symlink(string(change.ExpectedContent), change.Path)
|
||||
case FileChangeResolvedActionRecreateSymlink:
|
||||
if err := os.Remove(change.Path); err != nil {
|
||||
return fmt.Errorf("failed to remove symlink: %w", err)
|
||||
}
|
||||
return os.Symlink(string(change.ExpectedContent), change.Path)
|
||||
case FileChangeResolvedActionReorderSymlinks:
|
||||
return recreateSymlinks(change, nil)
|
||||
case FileChangeResolvedActionCreateDirectory:
|
||||
return os.MkdirAll(change.Path, 0755)
|
||||
case FileChangeResolvedActionRemove:
|
||||
return os.Remove(change.Path)
|
||||
case FileChangeResolvedActionRemoveDirectory:
|
||||
return os.RemoveAll(change.Path)
|
||||
case FileChangeResolvedActionTouch:
|
||||
return os.Chtimes(change.Path, time.Now(), time.Now())
|
||||
case FileChangeResolvedActionMountConfigFS:
|
||||
return mountConfigFS(change.Path)
|
||||
case FileChangeResolvedActionDoNothing:
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("unknown action: %d", change.Action())
|
||||
}
|
||||
}
|
||||
|
||||
func (c *ChangeSet) Apply() error {
|
||||
return c.ApplyChanges()
|
||||
}
|
|
@ -0,0 +1,115 @@
|
|||
//go:build arm && linux
|
||||
|
||||
package usbgadget
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var (
|
||||
usbConfig = &Config{
|
||||
VendorId: "0x1d6b", //The Linux Foundation
|
||||
ProductId: "0x0104", //Multifunction Composite Gadget
|
||||
SerialNumber: "",
|
||||
Manufacturer: "JetKVM",
|
||||
Product: "USB Emulation Device",
|
||||
strictMode: true,
|
||||
}
|
||||
usbDevices = &Devices{
|
||||
AbsoluteMouse: true,
|
||||
RelativeMouse: true,
|
||||
Keyboard: true,
|
||||
MassStorage: true,
|
||||
}
|
||||
usbGadgetName = "jetkvm"
|
||||
usbGadget *UsbGadget
|
||||
)
|
||||
|
||||
var oldAbsoluteMouseCombinedReportDesc = []byte{
|
||||
0x05, 0x01, // Usage Page (Generic Desktop Ctrls)
|
||||
0x09, 0x02, // Usage (Mouse)
|
||||
0xA1, 0x01, // Collection (Application)
|
||||
|
||||
// Report ID 1: Absolute Mouse Movement
|
||||
0x85, 0x01, // Report ID (1)
|
||||
0x09, 0x01, // Usage (Pointer)
|
||||
0xA1, 0x00, // Collection (Physical)
|
||||
0x05, 0x09, // Usage Page (Button)
|
||||
0x19, 0x01, // Usage Minimum (0x01)
|
||||
0x29, 0x03, // Usage Maximum (0x03)
|
||||
0x15, 0x00, // Logical Minimum (0)
|
||||
0x25, 0x01, // Logical Maximum (1)
|
||||
0x75, 0x01, // Report Size (1)
|
||||
0x95, 0x03, // Report Count (3)
|
||||
0x81, 0x02, // Input (Data, Var, Abs)
|
||||
0x95, 0x01, // Report Count (1)
|
||||
0x75, 0x05, // Report Size (5)
|
||||
0x81, 0x03, // Input (Cnst, Var, Abs)
|
||||
0x05, 0x01, // Usage Page (Generic Desktop Ctrls)
|
||||
0x09, 0x30, // Usage (X)
|
||||
0x09, 0x31, // Usage (Y)
|
||||
0x16, 0x00, 0x00, // Logical Minimum (0)
|
||||
0x26, 0xFF, 0x7F, // Logical Maximum (32767)
|
||||
0x36, 0x00, 0x00, // Physical Minimum (0)
|
||||
0x46, 0xFF, 0x7F, // Physical Maximum (32767)
|
||||
0x75, 0x10, // Report Size (16)
|
||||
0x95, 0x02, // Report Count (2)
|
||||
0x81, 0x02, // Input (Data, Var, Abs)
|
||||
0xC0, // End Collection
|
||||
|
||||
// Report ID 2: Relative Wheel Movement
|
||||
0x85, 0x02, // Report ID (2)
|
||||
0x09, 0x38, // Usage (Wheel)
|
||||
0x15, 0x81, // Logical Minimum (-127)
|
||||
0x25, 0x7F, // Logical Maximum (127)
|
||||
0x75, 0x08, // Report Size (8)
|
||||
0x95, 0x01, // Report Count (1)
|
||||
0x81, 0x06, // Input (Data, Var, Rel)
|
||||
|
||||
0xC0, // End Collection
|
||||
}
|
||||
|
||||
func TestUsbGadgetInit(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
usbGadget = NewUsbGadget(usbGadgetName, usbDevices, usbConfig, nil)
|
||||
|
||||
assert.NotNil(usbGadget)
|
||||
}
|
||||
|
||||
func TestUsbGadgetStrictModeInitFail(t *testing.T) {
|
||||
usbConfig.strictMode = true
|
||||
u := NewUsbGadget("test", usbDevices, usbConfig, nil)
|
||||
assert.Nil(t, u, "should be nil")
|
||||
}
|
||||
|
||||
func TestUsbGadgetUDCNotBoundAfterReportDescrChanged(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
usbGadget = NewUsbGadget(usbGadgetName, usbDevices, usbConfig, nil)
|
||||
assert.NotNil(usbGadget)
|
||||
|
||||
// release the usb gadget and create a new one
|
||||
usbGadget = nil
|
||||
|
||||
altGadgetConfig := defaultGadgetConfig
|
||||
|
||||
oldAbsoluteMouseConfig := altGadgetConfig["absolute_mouse"]
|
||||
oldAbsoluteMouseConfig.reportDesc = oldAbsoluteMouseCombinedReportDesc
|
||||
altGadgetConfig["absolute_mouse"] = oldAbsoluteMouseConfig
|
||||
|
||||
usbGadget = newUsbGadget(usbGadgetName, altGadgetConfig, usbDevices, usbConfig, nil)
|
||||
assert.NotNil(usbGadget)
|
||||
|
||||
udcs := getUdcs()
|
||||
assert.Equal(1, len(udcs), "should be only one UDC")
|
||||
// check if the UDC is bound
|
||||
udc := udcs[0]
|
||||
assert.NotNil(udc, "UDC should exist")
|
||||
|
||||
udcStr, err := os.ReadFile("/sys/kernel/config/usb_gadget/jetkvm/UDC")
|
||||
assert.Nil(err, "usb_gadget/UDC should exist")
|
||||
assert.Equal(strings.TrimSpace(udc), strings.TrimSpace(string(udcStr)), "UDC should be the same")
|
||||
}
|
|
@ -0,0 +1,183 @@
|
|||
package usbgadget
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/sourcegraph/tf-dag/dag"
|
||||
)
|
||||
|
||||
type ChangeSetResolver struct {
|
||||
changeset *ChangeSet
|
||||
|
||||
l *zerolog.Logger
|
||||
g *dag.AcyclicGraph
|
||||
|
||||
changesMap map[string]*FileChange
|
||||
conditionalChangesMap map[string]*FileChange
|
||||
|
||||
orderedChanges []dag.Vertex
|
||||
resolvedChanges []*FileChange
|
||||
additionalResolveRequired bool
|
||||
}
|
||||
|
||||
func (c *ChangeSetResolver) toOrderedChanges() error {
|
||||
for key, change := range c.changesMap {
|
||||
v := c.g.Add(key)
|
||||
|
||||
for _, dependsOn := range change.DependsOn {
|
||||
c.g.Connect(dag.BasicEdge(dependsOn, v))
|
||||
}
|
||||
for _, dependsOn := range change.resolvedDeps {
|
||||
c.g.Connect(dag.BasicEdge(dependsOn, v))
|
||||
}
|
||||
}
|
||||
|
||||
cycles := c.g.Cycles()
|
||||
if len(cycles) > 0 {
|
||||
return fmt.Errorf("cycles detected: %v", cycles)
|
||||
}
|
||||
|
||||
orderedChanges := c.g.TopologicalOrder()
|
||||
c.orderedChanges = orderedChanges
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ChangeSetResolver) doResolveChanges(initial bool) error {
|
||||
resolvedChanges := make([]*FileChange, 0)
|
||||
|
||||
for _, key := range c.orderedChanges {
|
||||
change := c.changesMap[key.(string)]
|
||||
if !initial {
|
||||
change.ResetActionResolution()
|
||||
}
|
||||
|
||||
resolvedAction := change.Action()
|
||||
|
||||
resolvedChanges = append(resolvedChanges, change)
|
||||
// no need to check the triggers if there's no change
|
||||
if resolvedAction == FileChangeResolvedActionDoNothing {
|
||||
continue
|
||||
}
|
||||
|
||||
if !initial {
|
||||
continue
|
||||
}
|
||||
|
||||
if change.BeforeChange != nil {
|
||||
change.resolvedDeps = append(change.resolvedDeps, change.BeforeChange...)
|
||||
c.additionalResolveRequired = true
|
||||
|
||||
// add the dependencies to the changes map
|
||||
for _, dep := range change.BeforeChange {
|
||||
depChange, ok := c.conditionalChangesMap[dep]
|
||||
if !ok {
|
||||
return fmt.Errorf("dependency %s not found", dep)
|
||||
}
|
||||
|
||||
c.changesMap[dep] = depChange
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.resolvedChanges = resolvedChanges
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ChangeSetResolver) resolveChanges(initial bool) error {
|
||||
// get the ordered changes
|
||||
err := c.toOrderedChanges()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// resolve the changes
|
||||
err = c.doResolveChanges(initial)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, change := range c.resolvedChanges {
|
||||
c.l.Trace().Str("change", change.String()).Msg("resolved change")
|
||||
}
|
||||
|
||||
if !c.additionalResolveRequired || !initial {
|
||||
return nil
|
||||
}
|
||||
|
||||
return c.resolveChanges(false)
|
||||
}
|
||||
|
||||
func (c *ChangeSetResolver) applyChanges() error {
|
||||
for _, change := range c.resolvedChanges {
|
||||
change.ResetActionResolution()
|
||||
action := change.Action()
|
||||
actionStr := FileChangeResolvedActionString[action]
|
||||
|
||||
l := c.l.Info()
|
||||
if action == FileChangeResolvedActionDoNothing {
|
||||
l = c.l.Trace()
|
||||
}
|
||||
|
||||
l.Str("action", actionStr).Str("change", change.String()).Msg("applying change")
|
||||
|
||||
err := c.changeset.applyChange(change)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ChangeSetResolver) GetChanges() ([]*FileChange, error) {
|
||||
localChanges := c.changeset.Changes
|
||||
changesMap := make(map[string]*FileChange)
|
||||
conditionalChangesMap := make(map[string]*FileChange)
|
||||
|
||||
// build the map of the changes
|
||||
for _, change := range localChanges {
|
||||
key := change.Key
|
||||
if key == "" {
|
||||
key = change.Path
|
||||
}
|
||||
|
||||
// remove it from the map first
|
||||
if change.When != "" {
|
||||
conditionalChangesMap[key] = &change
|
||||
continue
|
||||
}
|
||||
|
||||
if _, ok := changesMap[key]; ok {
|
||||
if changesMap[key].IsSame(&change.RequestedFileChange) {
|
||||
continue
|
||||
}
|
||||
return nil, fmt.Errorf(
|
||||
"duplicate change: %s, current: %s, requested: %s",
|
||||
key,
|
||||
changesMap[key].String(),
|
||||
change.String(),
|
||||
)
|
||||
}
|
||||
|
||||
changesMap[key] = &change
|
||||
}
|
||||
|
||||
c.changesMap = changesMap
|
||||
c.conditionalChangesMap = conditionalChangesMap
|
||||
|
||||
err := c.resolveChanges(true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c.resolvedChanges, nil
|
||||
}
|
||||
|
||||
func (c *ChangeSetResolver) Apply() error {
|
||||
if _, err := c.GetChanges(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.applyChanges()
|
||||
}
|
|
@ -0,0 +1,136 @@
|
|||
package usbgadget
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
type symlink struct {
|
||||
Path string
|
||||
Target string
|
||||
}
|
||||
|
||||
func compareSymlinks(expected []symlink, actual []symlink) bool {
|
||||
if len(expected) != len(actual) {
|
||||
return false
|
||||
}
|
||||
|
||||
return reflect.DeepEqual(expected, actual)
|
||||
}
|
||||
|
||||
func checkIfSymlinksInOrder(fc *FileChange, logger *zerolog.Logger) (FileState, error) {
|
||||
if logger == nil {
|
||||
logger = defaultLogger
|
||||
}
|
||||
l := logger.With().Str("path", fc.Path).Logger()
|
||||
|
||||
if len(fc.ParamSymlinks) == 0 {
|
||||
return FileStateUnknown, fmt.Errorf("no symlinks to check")
|
||||
}
|
||||
|
||||
fi, err := os.Lstat(fc.Path)
|
||||
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return FileStateAbsent, nil
|
||||
} else {
|
||||
l.Warn().Err(err).Msg("failed to stat file")
|
||||
return FileStateUnknown, fmt.Errorf("failed to stat file")
|
||||
}
|
||||
}
|
||||
|
||||
if !fi.IsDir() {
|
||||
return FileStateUnknown, fmt.Errorf("file is not a directory")
|
||||
}
|
||||
|
||||
files, err := os.ReadDir(fc.Path)
|
||||
symlinks := make([]symlink, 0)
|
||||
if err != nil {
|
||||
return FileStateUnknown, fmt.Errorf("failed to read directory")
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
if file.Type()&os.ModeSymlink != os.ModeSymlink {
|
||||
continue
|
||||
}
|
||||
|
||||
path := filepath.Join(fc.Path, file.Name())
|
||||
target, err := os.Readlink(path)
|
||||
if err != nil {
|
||||
return FileStateUnknown, fmt.Errorf("failed to read symlink")
|
||||
}
|
||||
|
||||
if !filepath.IsAbs(target) {
|
||||
target = filepath.Join(fc.Path, target)
|
||||
newTarget, err := filepath.Abs(target)
|
||||
if err != nil {
|
||||
return FileStateUnknown, fmt.Errorf("failed to get absolute path")
|
||||
}
|
||||
target = newTarget
|
||||
}
|
||||
|
||||
symlinks = append(symlinks, symlink{
|
||||
Path: path,
|
||||
Target: target,
|
||||
})
|
||||
}
|
||||
|
||||
// compare the symlinks with the expected symlinks
|
||||
if compareSymlinks(fc.ParamSymlinks, symlinks) {
|
||||
return FileStateSymlinkInOrderConfigFS, nil
|
||||
}
|
||||
|
||||
l.Trace().Interface("expected", fc.ParamSymlinks).Interface("actual", symlinks).Msg("symlinks are not in order")
|
||||
|
||||
return FileStateSymlinkNotInOrderConfigFS, nil
|
||||
}
|
||||
|
||||
func recreateSymlinks(fc *FileChange, logger *zerolog.Logger) error {
|
||||
if logger == nil {
|
||||
logger = defaultLogger
|
||||
}
|
||||
// remove all symlinks
|
||||
files, err := os.ReadDir(fc.Path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read directory")
|
||||
}
|
||||
|
||||
l := logger.With().Str("path", fc.Path).Logger()
|
||||
l.Info().Msg("recreate symlinks")
|
||||
|
||||
for _, file := range files {
|
||||
if file.Type()&os.ModeSymlink != os.ModeSymlink {
|
||||
continue
|
||||
}
|
||||
l.Info().Str("name", file.Name()).Msg("remove symlink")
|
||||
err := os.Remove(path.Join(fc.Path, file.Name()))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to remove symlink")
|
||||
}
|
||||
}
|
||||
|
||||
l.Info().Interface("param-symlinks", fc.ParamSymlinks).Msg("create symlinks")
|
||||
|
||||
// create the symlinks
|
||||
for _, symlink := range fc.ParamSymlinks {
|
||||
l.Info().Str("name", symlink.Path).Str("target", symlink.Target).Msg("create symlink")
|
||||
|
||||
path := symlink.Path
|
||||
if !filepath.IsAbs(path) {
|
||||
path = filepath.Join(fc.Path, path)
|
||||
}
|
||||
|
||||
err := os.Symlink(symlink.Target, path)
|
||||
if err != nil {
|
||||
l.Warn().Err(err).Msg("failed to create symlink")
|
||||
return fmt.Errorf("failed to create symlink")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -2,11 +2,7 @@ package usbgadget
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
)
|
||||
|
||||
type gadgetConfigItem struct {
|
||||
|
@ -160,20 +156,10 @@ func (u *UsbGadget) OverrideGadgetConfig(itemKey string, itemAttr string, value
|
|||
return nil, true
|
||||
}
|
||||
|
||||
func mountConfigFS() error {
|
||||
_, err := os.Stat(gadgetPath)
|
||||
// TODO: check if it's mounted properly
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if os.IsNotExist(err) {
|
||||
err = exec.Command("mount", "-t", "configfs", "none", configFSPath).Run()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to mount configfs: %w", err)
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("unable to access usb gadget path: %w", err)
|
||||
func mountConfigFS(path string) error {
|
||||
err := exec.Command("mount", "-t", "configfs", "none", path).Run()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to mount configfs: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -186,26 +172,19 @@ func (u *UsbGadget) Init() error {
|
|||
|
||||
udcs := getUdcs()
|
||||
if len(udcs) < 1 {
|
||||
u.log.Error().Msg("no udc found, skipping USB stack init")
|
||||
return nil
|
||||
return u.logWarn("no udc found, skipping USB stack init", nil)
|
||||
}
|
||||
|
||||
u.udc = udcs[0]
|
||||
_, err := os.Stat(u.kvmGadgetPath)
|
||||
if err == nil {
|
||||
u.log.Info().Msg("usb gadget already exists")
|
||||
}
|
||||
|
||||
if err := mountConfigFS(); err != nil {
|
||||
u.log.Error().Err(err).Msg("failed to mount configfs, usb stack might not function properly")
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(u.configC1Path, 0755); err != nil {
|
||||
u.log.Error().Err(err).Msg("failed to create config path")
|
||||
}
|
||||
|
||||
if err := u.writeGadgetConfig(); err != nil {
|
||||
u.log.Error().Err(err).Msg("failed to start gadget")
|
||||
err := u.WithTransaction(func() error {
|
||||
u.tx.MountConfigFS()
|
||||
u.tx.CreateConfigPath()
|
||||
u.tx.WriteGadgetConfig()
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return u.logError("unable to initialize USB stack", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -217,143 +196,13 @@ func (u *UsbGadget) UpdateGadgetConfig() error {
|
|||
|
||||
u.loadGadgetConfig()
|
||||
|
||||
if err := u.writeGadgetConfig(); err != nil {
|
||||
u.log.Error().Err(err).Msg("failed to update gadget")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *UsbGadget) getOrderedConfigItems() orderedGadgetConfigItems {
|
||||
items := make([]gadgetConfigItemWithKey, 0)
|
||||
for key, item := range u.configMap {
|
||||
items = append(items, gadgetConfigItemWithKey{key, item})
|
||||
}
|
||||
|
||||
sort.Slice(items, func(i, j int) bool {
|
||||
return items[i].item.order < items[j].item.order
|
||||
err := u.WithTransaction(func() error {
|
||||
u.tx.WriteGadgetConfig()
|
||||
return nil
|
||||
})
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
func (u *UsbGadget) writeGadgetConfig() error {
|
||||
// create kvm gadget path
|
||||
err := os.MkdirAll(u.kvmGadgetPath, 0755)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
u.log.Trace().Msg("writing gadget config")
|
||||
for _, val := range u.getOrderedConfigItems() {
|
||||
key := val.key
|
||||
item := val.item
|
||||
|
||||
// check if the item is enabled in the config
|
||||
if !u.isGadgetConfigItemEnabled(key) {
|
||||
u.log.Trace().Str("key", key).Msg("disabling gadget config")
|
||||
err = u.disableGadgetItemConfig(item)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
u.log.Trace().Str("key", key).Msg("writing gadget config")
|
||||
err = u.writeGadgetItemConfig(item)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err = u.writeUDC(); err != nil {
|
||||
u.log.Error().Err(err).Msg("failed to write UDC")
|
||||
return err
|
||||
}
|
||||
|
||||
if err = u.rebindUsb(true); err != nil {
|
||||
u.log.Info().Err(err).Msg("failed to rebind usb")
|
||||
return u.logError("unable to update gadget config", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *UsbGadget) disableGadgetItemConfig(item gadgetConfigItem) error {
|
||||
// remove symlink if exists
|
||||
if item.configPath == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
configPath := joinPath(u.configC1Path, item.configPath)
|
||||
|
||||
if _, err := os.Lstat(configPath); os.IsNotExist(err) {
|
||||
u.log.Trace().Str("path", configPath).Msg("symlink does not exist")
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := os.Remove(configPath); err != nil {
|
||||
return fmt.Errorf("failed to remove symlink %s: %w", item.configPath, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *UsbGadget) writeGadgetItemConfig(item gadgetConfigItem) error {
|
||||
// create directory for the item
|
||||
gadgetItemPath := joinPath(u.kvmGadgetPath, item.path)
|
||||
err := os.MkdirAll(gadgetItemPath, 0755)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create path %s: %w", gadgetItemPath, err)
|
||||
}
|
||||
|
||||
if len(item.attrs) > 0 {
|
||||
// write attributes for the item
|
||||
err = u.writeGadgetAttrs(gadgetItemPath, item.attrs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write attributes for %s: %w", gadgetItemPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
// write report descriptor if available
|
||||
if item.reportDesc != nil {
|
||||
err = u.writeIfDifferent(path.Join(gadgetItemPath, "report_desc"), item.reportDesc, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// create config directory if configAttrs are set
|
||||
if len(item.configAttrs) > 0 {
|
||||
configItemPath := joinPath(u.configC1Path, item.configPath)
|
||||
err = os.MkdirAll(configItemPath, 0755)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create path %s: %w", configItemPath, err)
|
||||
}
|
||||
|
||||
err = u.writeGadgetAttrs(configItemPath, item.configAttrs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write config attributes for %s: %w", configItemPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
// create symlink if configPath is set
|
||||
if item.configPath != nil && item.configAttrs == nil {
|
||||
configPath := joinPath(u.configC1Path, item.configPath)
|
||||
u.log.Trace().Str("source", configPath).Str("target", gadgetItemPath).Msg("creating symlink")
|
||||
if err := ensureSymlink(configPath, gadgetItemPath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *UsbGadget) writeGadgetAttrs(basePath string, attrs gadgetAttributes) error {
|
||||
for key, val := range attrs {
|
||||
filePath := filepath.Join(basePath, key)
|
||||
err := u.writeIfDifferent(filePath, []byte(val), 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write to %s: %w", filePath, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -0,0 +1,347 @@
|
|||
package usbgadget
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
// no os package should occur in this file
|
||||
|
||||
type UsbGadgetTransaction struct {
|
||||
c *ChangeSet
|
||||
|
||||
// below are the fields that are needed to be set by the caller
|
||||
log *zerolog.Logger
|
||||
udc string
|
||||
dwc3Path string
|
||||
kvmGadgetPath string
|
||||
configC1Path string
|
||||
orderedConfigItems orderedGadgetConfigItems
|
||||
isGadgetConfigItemEnabled func(key string) bool
|
||||
|
||||
reorderSymlinkChanges *RequestedFileChange
|
||||
}
|
||||
|
||||
func (u *UsbGadget) newUsbGadgetTransaction(lock bool) error {
|
||||
if lock {
|
||||
u.txLock.Lock()
|
||||
defer u.txLock.Unlock()
|
||||
}
|
||||
|
||||
if u.tx != nil {
|
||||
return fmt.Errorf("transaction already exists")
|
||||
}
|
||||
|
||||
tx := &UsbGadgetTransaction{
|
||||
c: &ChangeSet{},
|
||||
log: u.log,
|
||||
udc: u.udc,
|
||||
dwc3Path: dwc3Path,
|
||||
kvmGadgetPath: u.kvmGadgetPath,
|
||||
configC1Path: u.configC1Path,
|
||||
orderedConfigItems: u.getOrderedConfigItems(),
|
||||
isGadgetConfigItemEnabled: u.isGadgetConfigItemEnabled,
|
||||
}
|
||||
u.tx = tx
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *UsbGadget) WithTransaction(fn func() error) error {
|
||||
u.txLock.Lock()
|
||||
defer u.txLock.Unlock()
|
||||
|
||||
err := u.newUsbGadgetTransaction(false)
|
||||
if err != nil {
|
||||
u.log.Error().Err(err).Msg("failed to create transaction")
|
||||
return err
|
||||
}
|
||||
if err := fn(); err != nil {
|
||||
u.log.Error().Err(err).Msg("transaction failed")
|
||||
return err
|
||||
}
|
||||
result := u.tx.Commit()
|
||||
u.tx = nil
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (tx *UsbGadgetTransaction) addFileChange(component string, change RequestedFileChange) string {
|
||||
change.Component = component
|
||||
tx.c.AddFileChangeStruct(change)
|
||||
|
||||
key := change.Key
|
||||
if key == "" {
|
||||
key = change.Path
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
func (tx *UsbGadgetTransaction) mkdirAll(component string, path string, description string, deps []string) string {
|
||||
return tx.addFileChange(component, RequestedFileChange{
|
||||
Path: path,
|
||||
ExpectedState: FileStateDirectory,
|
||||
Description: description,
|
||||
DependsOn: deps,
|
||||
})
|
||||
}
|
||||
|
||||
func (tx *UsbGadgetTransaction) removeFile(component string, path string, description string) string {
|
||||
return tx.addFileChange(component, RequestedFileChange{
|
||||
Path: path,
|
||||
ExpectedState: FileStateAbsent,
|
||||
Description: description,
|
||||
})
|
||||
}
|
||||
|
||||
func (tx *UsbGadgetTransaction) Commit() error {
|
||||
tx.addFileChange("gadget-finalize", *tx.reorderSymlinkChanges)
|
||||
|
||||
err := tx.c.Apply()
|
||||
if err != nil {
|
||||
tx.log.Error().Err(err).Msg("failed to update usbgadget configuration")
|
||||
return err
|
||||
}
|
||||
tx.log.Info().Msg("usbgadget configuration updated")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *UsbGadget) getOrderedConfigItems() orderedGadgetConfigItems {
|
||||
items := make([]gadgetConfigItemWithKey, 0)
|
||||
for key, item := range u.configMap {
|
||||
items = append(items, gadgetConfigItemWithKey{key, item})
|
||||
}
|
||||
|
||||
sort.Slice(items, func(i, j int) bool {
|
||||
return items[i].item.order < items[j].item.order
|
||||
})
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
func (tx *UsbGadgetTransaction) MountConfigFS() {
|
||||
tx.addFileChange("gadget", RequestedFileChange{
|
||||
Path: configFSPath,
|
||||
ExpectedState: FileStateMountedConfigFS,
|
||||
Description: "mount configfs",
|
||||
})
|
||||
}
|
||||
|
||||
func (tx *UsbGadgetTransaction) CreateConfigPath() {
|
||||
tx.mkdirAll(
|
||||
"gadget",
|
||||
tx.configC1Path,
|
||||
"create config path",
|
||||
[]string{configFSPath},
|
||||
)
|
||||
}
|
||||
|
||||
func (tx *UsbGadgetTransaction) WriteGadgetConfig() {
|
||||
// create kvm gadget path
|
||||
tx.mkdirAll(
|
||||
"gadget",
|
||||
tx.kvmGadgetPath,
|
||||
"create kvm gadget path",
|
||||
[]string{tx.configC1Path},
|
||||
)
|
||||
|
||||
deps := make([]string, 0)
|
||||
deps = append(deps, tx.kvmGadgetPath)
|
||||
|
||||
for _, val := range tx.orderedConfigItems {
|
||||
key := val.key
|
||||
item := val.item
|
||||
|
||||
// check if the item is enabled in the config
|
||||
if !tx.isGadgetConfigItemEnabled(key) {
|
||||
tx.DisableGadgetItemConfig(item)
|
||||
continue
|
||||
}
|
||||
deps = tx.writeGadgetItemConfig(item, deps)
|
||||
}
|
||||
|
||||
tx.WriteUDC()
|
||||
}
|
||||
|
||||
func (tx *UsbGadgetTransaction) getDisableKeys() []string {
|
||||
disableKeys := make([]string, 0)
|
||||
for _, item := range tx.orderedConfigItems {
|
||||
if !tx.isGadgetConfigItemEnabled(item.key) {
|
||||
continue
|
||||
}
|
||||
if item.item.configPath == nil || item.item.configAttrs != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
disableKeys = append(disableKeys, fmt.Sprintf("disable-%s", item.item.device))
|
||||
}
|
||||
return disableKeys
|
||||
}
|
||||
|
||||
func (tx *UsbGadgetTransaction) DisableGadgetItemConfig(item gadgetConfigItem) {
|
||||
// remove symlink if exists
|
||||
if item.configPath == nil {
|
||||
return
|
||||
}
|
||||
|
||||
configPath := joinPath(tx.configC1Path, item.configPath)
|
||||
_ = tx.removeFile("gadget", configPath, "remove symlink: disable gadget config")
|
||||
}
|
||||
|
||||
func (tx *UsbGadgetTransaction) writeGadgetItemConfig(item gadgetConfigItem, deps []string) []string {
|
||||
component := item.device
|
||||
|
||||
// create directory for the item
|
||||
files := make([]string, 0)
|
||||
files = append(files, deps...)
|
||||
|
||||
gadgetItemPath := joinPath(tx.kvmGadgetPath, item.path)
|
||||
if gadgetItemPath != tx.kvmGadgetPath {
|
||||
gadgetItemDir := tx.mkdirAll(component, gadgetItemPath, "create gadget item directory", files)
|
||||
files = append(files, gadgetItemDir)
|
||||
}
|
||||
|
||||
beforeChange := make([]string, 0)
|
||||
disableGadgetItemKey := fmt.Sprintf("disable-%s", item.device)
|
||||
if item.configPath != nil && item.configAttrs == nil {
|
||||
beforeChange = append(beforeChange, tx.getDisableKeys()...)
|
||||
}
|
||||
|
||||
if len(item.attrs) > 0 {
|
||||
// write attributes for the item
|
||||
files = append(files, tx.writeGadgetAttrs(
|
||||
gadgetItemPath,
|
||||
item.attrs,
|
||||
component,
|
||||
beforeChange,
|
||||
)...)
|
||||
}
|
||||
|
||||
// write report descriptor if available
|
||||
reportDescPath := path.Join(gadgetItemPath, "report_desc")
|
||||
if item.reportDesc != nil {
|
||||
tx.addFileChange(component, RequestedFileChange{
|
||||
Path: reportDescPath,
|
||||
ExpectedState: FileStateFileContentMatch,
|
||||
ExpectedContent: item.reportDesc,
|
||||
Description: "write report descriptor",
|
||||
BeforeChange: beforeChange,
|
||||
DependsOn: files,
|
||||
})
|
||||
} else {
|
||||
tx.addFileChange(component, RequestedFileChange{
|
||||
Path: reportDescPath,
|
||||
ExpectedState: FileStateAbsent,
|
||||
Description: "remove report descriptor",
|
||||
BeforeChange: beforeChange,
|
||||
DependsOn: files,
|
||||
})
|
||||
}
|
||||
files = append(files, reportDescPath)
|
||||
|
||||
// create config directory if configAttrs are set
|
||||
if len(item.configAttrs) > 0 {
|
||||
configItemPath := joinPath(tx.configC1Path, item.configPath)
|
||||
if configItemPath != tx.configC1Path {
|
||||
configItemDir := tx.mkdirAll(component, configItemPath, "create config item directory", files)
|
||||
files = append(files, configItemDir)
|
||||
}
|
||||
files = append(files, tx.writeGadgetAttrs(
|
||||
configItemPath,
|
||||
item.configAttrs,
|
||||
component,
|
||||
beforeChange,
|
||||
)...)
|
||||
}
|
||||
|
||||
// create symlink if configPath is set
|
||||
if item.configPath != nil && item.configAttrs == nil {
|
||||
configPath := joinPath(tx.configC1Path, item.configPath)
|
||||
|
||||
// the change will be only applied by `beforeChange`
|
||||
tx.addFileChange(component, RequestedFileChange{
|
||||
Key: disableGadgetItemKey,
|
||||
Path: configPath,
|
||||
ExpectedState: FileStateAbsent,
|
||||
When: "beforeChange", // TODO: make it more flexible
|
||||
Description: "remove symlink",
|
||||
})
|
||||
|
||||
tx.addReorderSymlinkChange(configPath, gadgetItemPath, files)
|
||||
}
|
||||
|
||||
return files
|
||||
}
|
||||
|
||||
func (tx *UsbGadgetTransaction) writeGadgetAttrs(basePath string, attrs gadgetAttributes, component string, beforeChange []string) (files []string) {
|
||||
files = make([]string, 0)
|
||||
for key, val := range attrs {
|
||||
filePath := filepath.Join(basePath, key)
|
||||
tx.addFileChange(component, RequestedFileChange{
|
||||
Path: filePath,
|
||||
ExpectedState: FileStateFileContentMatch,
|
||||
ExpectedContent: []byte(val),
|
||||
Description: "write gadget attribute",
|
||||
DependsOn: []string{basePath},
|
||||
BeforeChange: beforeChange,
|
||||
})
|
||||
files = append(files, filePath)
|
||||
}
|
||||
return files
|
||||
}
|
||||
|
||||
func (tx *UsbGadgetTransaction) addReorderSymlinkChange(path string, target string, deps []string) {
|
||||
tx.log.Trace().Str("path", path).Str("target", target).Msg("add reorder symlink change")
|
||||
|
||||
if tx.reorderSymlinkChanges == nil {
|
||||
tx.reorderSymlinkChanges = &RequestedFileChange{
|
||||
Component: "gadget-finalize",
|
||||
Key: "reorder-symlinks",
|
||||
Path: tx.configC1Path,
|
||||
ExpectedState: FileStateSymlinkInOrderConfigFS,
|
||||
Description: "order symlinks",
|
||||
ParamSymlinks: []symlink{},
|
||||
}
|
||||
}
|
||||
|
||||
tx.reorderSymlinkChanges.DependsOn = append(tx.reorderSymlinkChanges.DependsOn, deps...)
|
||||
tx.reorderSymlinkChanges.ParamSymlinks = append(tx.reorderSymlinkChanges.ParamSymlinks, symlink{
|
||||
Path: path,
|
||||
Target: target,
|
||||
})
|
||||
}
|
||||
|
||||
func (tx *UsbGadgetTransaction) WriteUDC() {
|
||||
// bound the gadget to a UDC (USB Device Controller)
|
||||
path := path.Join(tx.kvmGadgetPath, "UDC")
|
||||
tx.addFileChange("udc", RequestedFileChange{
|
||||
Path: path,
|
||||
ExpectedState: FileStateFileContentMatch,
|
||||
ExpectedContent: []byte(tx.udc),
|
||||
DependsOn: []string{"reorder-symlinks"},
|
||||
Description: "write UDC",
|
||||
})
|
||||
}
|
||||
|
||||
func (tx *UsbGadgetTransaction) RebindUsb(ignoreUnbindError bool) {
|
||||
// remove the gadget from the UDC
|
||||
tx.addFileChange("udc", RequestedFileChange{
|
||||
Path: path.Join(tx.dwc3Path, "unbind"),
|
||||
ExpectedState: FileStateFileWrite,
|
||||
ExpectedContent: []byte(tx.udc),
|
||||
Description: "unbind UDC",
|
||||
})
|
||||
// bind the gadget to the UDC
|
||||
tx.addFileChange("udc", RequestedFileChange{
|
||||
Path: path.Join(tx.dwc3Path, "bind"),
|
||||
ExpectedState: FileStateFileWrite,
|
||||
ExpectedContent: []byte(tx.udc),
|
||||
Description: "bind UDC",
|
||||
DependsOn: []string{path.Join(tx.dwc3Path, "unbind")},
|
||||
IgnoreErrors: ignoreUnbindError,
|
||||
})
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package usbgadget
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
func (u *UsbGadget) logWarn(msg string, err error) error {
|
||||
if err == nil {
|
||||
err = errors.New(msg)
|
||||
}
|
||||
if u.strictMode {
|
||||
return err
|
||||
}
|
||||
u.log.Warn().Err(err).Msg(msg)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *UsbGadget) logError(msg string, err error) error {
|
||||
if err == nil {
|
||||
err = errors.New(msg)
|
||||
}
|
||||
if u.strictMode {
|
||||
return err
|
||||
}
|
||||
u.log.Error().Err(err).Msg(msg)
|
||||
return nil
|
||||
}
|
|
@ -50,18 +50,6 @@ func (u *UsbGadget) RebindUsb(ignoreUnbindError bool) error {
|
|||
return u.rebindUsb(ignoreUnbindError)
|
||||
}
|
||||
|
||||
func (u *UsbGadget) writeUDC() error {
|
||||
path := path.Join(u.kvmGadgetPath, "UDC")
|
||||
|
||||
u.log.Trace().Str("udc", u.udc).Str("path", path).Msg("writing UDC")
|
||||
err := u.writeIfDifferent(path, []byte(u.udc), 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write UDC: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetUsbState returns the current state of the USB gadget
|
||||
func (u *UsbGadget) GetUsbState() (state string) {
|
||||
stateFile := path.Join("/sys/class/udc", u.udc, "state")
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/jetkvm/kvm/internal/logging"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
|
@ -28,7 +29,8 @@ type Config struct {
|
|||
Manufacturer string `json:"manufacturer"`
|
||||
Product string `json:"product"`
|
||||
|
||||
isEmpty bool
|
||||
strictMode bool // when it's enabled, all warnings will be converted to errors
|
||||
isEmpty bool
|
||||
}
|
||||
|
||||
var defaultUsbGadgetDevices = Devices{
|
||||
|
@ -59,22 +61,31 @@ type UsbGadget struct {
|
|||
|
||||
enabledDevices Devices
|
||||
|
||||
strictMode bool // only intended for testing for now
|
||||
|
||||
absMouseAccumulatedWheelY float64
|
||||
|
||||
lastUserInput time.Time
|
||||
|
||||
tx *UsbGadgetTransaction
|
||||
txLock sync.Mutex
|
||||
|
||||
log *zerolog.Logger
|
||||
}
|
||||
|
||||
const configFSPath = "/sys/kernel/config"
|
||||
const gadgetPath = "/sys/kernel/config/usb_gadget"
|
||||
|
||||
var defaultLogger = zerolog.New(os.Stdout).Level(zerolog.InfoLevel)
|
||||
var defaultLogger = logging.GetSubsystemLogger("usbgadget")
|
||||
|
||||
// NewUsbGadget creates a new UsbGadget.
|
||||
func NewUsbGadget(name string, enabledDevices *Devices, config *Config, logger *zerolog.Logger) *UsbGadget {
|
||||
return newUsbGadget(name, defaultGadgetConfig, enabledDevices, config, logger)
|
||||
}
|
||||
|
||||
func newUsbGadget(name string, configMap map[string]gadgetConfigItem, enabledDevices *Devices, config *Config, logger *zerolog.Logger) *UsbGadget {
|
||||
if logger == nil {
|
||||
logger = &defaultLogger
|
||||
logger = defaultLogger
|
||||
}
|
||||
|
||||
if enabledDevices == nil {
|
||||
|
@ -89,16 +100,19 @@ func NewUsbGadget(name string, enabledDevices *Devices, config *Config, logger *
|
|||
name: name,
|
||||
kvmGadgetPath: path.Join(gadgetPath, name),
|
||||
configC1Path: path.Join(gadgetPath, name, "configs/c.1"),
|
||||
configMap: defaultGadgetConfig,
|
||||
configMap: configMap,
|
||||
customConfig: *config,
|
||||
configLock: sync.Mutex{},
|
||||
keyboardLock: sync.Mutex{},
|
||||
absMouseLock: sync.Mutex{},
|
||||
relMouseLock: sync.Mutex{},
|
||||
txLock: sync.Mutex{},
|
||||
enabledDevices: *enabledDevices,
|
||||
lastUserInput: time.Now(),
|
||||
log: logger,
|
||||
|
||||
strictMode: config.strictMode,
|
||||
|
||||
absMouseAccumulatedWheelY: 0,
|
||||
}
|
||||
if err := g.Init(); err != nil {
|
||||
|
|
|
@ -3,8 +3,9 @@ package usbgadget
|
|||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func joinPath(basePath string, paths []string) string {
|
||||
|
@ -12,44 +13,68 @@ func joinPath(basePath string, paths []string) string {
|
|||
return filepath.Join(pathArr...)
|
||||
}
|
||||
|
||||
func ensureSymlink(linkPath string, target string) error {
|
||||
if _, err := os.Lstat(linkPath); err == nil {
|
||||
currentTarget, err := os.Readlink(linkPath)
|
||||
if err != nil || currentTarget != target {
|
||||
err = os.Remove(linkPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to remove existing symlink %s: %w", linkPath, err)
|
||||
}
|
||||
}
|
||||
} else if !os.IsNotExist(err) {
|
||||
return fmt.Errorf("failed to check if symlink exists: %w", err)
|
||||
func hexToDecimal(hex string) (int64, error) {
|
||||
decimal, err := strconv.ParseInt(hex, 16, 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if err := os.Symlink(target, linkPath); err != nil {
|
||||
return fmt.Errorf("failed to create symlink from %s to %s: %w", linkPath, target, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
return decimal, nil
|
||||
}
|
||||
|
||||
func (u *UsbGadget) writeIfDifferent(filePath string, content []byte, permMode os.FileMode) error {
|
||||
if _, err := os.Stat(filePath); err == nil {
|
||||
oldContent, err := os.ReadFile(filePath)
|
||||
if err == nil {
|
||||
if bytes.Equal(oldContent, content) {
|
||||
u.log.Trace().Str("path", filePath).Msg("skipping writing to as it already has the correct content")
|
||||
return nil
|
||||
}
|
||||
func decimalToOctal(decimal int64) string {
|
||||
return fmt.Sprintf("%04o", decimal)
|
||||
}
|
||||
|
||||
if len(oldContent) == len(content)+1 &&
|
||||
bytes.Equal(oldContent[:len(content)], content) &&
|
||||
oldContent[len(content)] == 10 {
|
||||
u.log.Trace().Str("path", filePath).Msg("skipping writing to as it already has the correct content")
|
||||
return nil
|
||||
}
|
||||
func hexToOctal(hex string) (string, error) {
|
||||
hex = strings.ToLower(hex)
|
||||
hex = strings.Replace(hex, "0x", "", 1) //remove 0x or 0X
|
||||
|
||||
u.log.Trace().Str("path", filePath).Bytes("old", oldContent).Bytes("new", content).Msg("writing to as it has different content")
|
||||
decimal, err := hexToDecimal(hex)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Convert the decimal integer to an octal string.
|
||||
octal := decimalToOctal(decimal)
|
||||
return octal, nil
|
||||
}
|
||||
|
||||
func compareFileContent(oldContent []byte, newContent []byte, looserMatch bool) bool {
|
||||
if bytes.Equal(oldContent, newContent) {
|
||||
return true
|
||||
}
|
||||
|
||||
if len(oldContent) == len(newContent)+1 &&
|
||||
bytes.Equal(oldContent[:len(newContent)], newContent) &&
|
||||
oldContent[len(newContent)] == 10 {
|
||||
return true
|
||||
}
|
||||
|
||||
if len(newContent) == 4 {
|
||||
if len(oldContent) < 6 || len(oldContent) > 7 {
|
||||
return false
|
||||
}
|
||||
|
||||
if len(oldContent) == 7 && oldContent[6] == 0x0a {
|
||||
oldContent = oldContent[:6]
|
||||
}
|
||||
|
||||
oldOctalValue, err := hexToOctal(string(oldContent))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if oldOctalValue == string(newContent) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return os.WriteFile(filePath, content, permMode)
|
||||
|
||||
if looserMatch {
|
||||
oldContentStr := strings.TrimSpace(string(oldContent))
|
||||
newContentStr := strings.TrimSpace(string(newContent))
|
||||
|
||||
return oldContentStr == newContentStr
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
|
17
jsonrpc.go
17
jsonrpc.go
|
@ -877,14 +877,15 @@ func rpcSetCloudUrl(apiUrl string, appUrl string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
var currentScrollSensitivity string = "default"
|
||||
|
||||
func rpcGetScrollSensitivity() (string, error) {
|
||||
return currentScrollSensitivity, nil
|
||||
func rpcGetKeyboardLayout() (string, error) {
|
||||
return config.KeyboardLayout, nil
|
||||
}
|
||||
|
||||
func rpcSetScrollSensitivity(sensitivity string) error {
|
||||
currentScrollSensitivity = sensitivity
|
||||
func rpcSetKeyboardLayout(layout string) error {
|
||||
config.KeyboardLayout = layout
|
||||
if err := SaveConfig(); err != nil {
|
||||
return fmt.Errorf("failed to save config: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -1053,8 +1054,8 @@ var rpcHandlers = map[string]RPCHandler{
|
|||
"setUsbDevices": {Func: rpcSetUsbDevices, Params: []string{"devices"}},
|
||||
"setUsbDeviceState": {Func: rpcSetUsbDeviceState, Params: []string{"device", "enabled"}},
|
||||
"setCloudUrl": {Func: rpcSetCloudUrl, Params: []string{"apiUrl", "appUrl"}},
|
||||
"getScrollSensitivity": {Func: rpcGetScrollSensitivity},
|
||||
"setScrollSensitivity": {Func: rpcSetScrollSensitivity, Params: []string{"sensitivity"}},
|
||||
"getKeyboardLayout": {Func: rpcGetKeyboardLayout},
|
||||
"setKeyboardLayout": {Func: rpcSetKeyboardLayout, Params: []string{"layout"}},
|
||||
"getKeyboardMacros": {Func: getKeyboardMacros},
|
||||
"setKeyboardMacros": {Func: setKeyboardMacros, Params: []string{"params"}},
|
||||
}
|
||||
|
|
|
@ -29,8 +29,6 @@ runTest() {
|
|||
function exit_with_code() {
|
||||
if [ $EXIT_CODE -ne 0 ]; then
|
||||
printf "\e[0;31m❌ Test failed\e[0m\n"
|
||||
else
|
||||
printf "\e[0;32m✅ All tests passed\e[0m\n"
|
||||
fi
|
||||
|
||||
exit $EXIT_CODE
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -2,7 +2,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|||
import { useResizeObserver } from "usehooks-ts";
|
||||
|
||||
import {
|
||||
useDeviceSettingsStore,
|
||||
useHidStore,
|
||||
useMouseStore,
|
||||
useRTCStore,
|
||||
|
@ -61,7 +60,6 @@ export default function WebRTCVideo() {
|
|||
useHidStore();
|
||||
|
||||
// Misc states and hooks
|
||||
const [blockWheelEvent, setBlockWheelEvent] = useState(false);
|
||||
const disableVideoFocusTrap = useUiStore(state => state.disableVideoFocusTrap);
|
||||
const [send] = useJsonRpc();
|
||||
|
||||
|
@ -248,17 +246,8 @@ export default function WebRTCVideo() {
|
|||
],
|
||||
);
|
||||
|
||||
const trackpadSensitivity = useDeviceSettingsStore(state => state.trackpadSensitivity);
|
||||
const mouseSensitivity = useDeviceSettingsStore(state => state.mouseSensitivity);
|
||||
const clampMin = useDeviceSettingsStore(state => state.clampMin);
|
||||
const clampMax = useDeviceSettingsStore(state => state.clampMax);
|
||||
const blockDelay = useDeviceSettingsStore(state => state.blockDelay);
|
||||
const trackpadThreshold = useDeviceSettingsStore(state => state.trackpadThreshold);
|
||||
|
||||
const mouseWheelHandler = useCallback(
|
||||
(e: WheelEvent) => {
|
||||
if (blockWheelEvent) return;
|
||||
|
||||
// Determine if the wheel event is an accel scroll value
|
||||
const isAccel = Math.abs(e.deltaY) >= 100;
|
||||
|
||||
|
@ -266,7 +255,7 @@ export default function WebRTCVideo() {
|
|||
const accelScrollValue = e.deltaY / 100;
|
||||
|
||||
// Calculate the no accel scroll value
|
||||
const noAccelScrollValue = e.deltaY > 0 ? 1 : (e.deltaY < 0 ? -1 : 0);
|
||||
const noAccelScrollValue = e.deltaY > 0 ? 1 : e.deltaY < 0 ? -1 : 0;
|
||||
|
||||
// Get scroll value
|
||||
const scrollValue = isAccel ? accelScrollValue : noAccelScrollValue;
|
||||
|
@ -277,22 +266,9 @@ export default function WebRTCVideo() {
|
|||
// Invert the clamped scroll value to match expected behavior
|
||||
const invertedScrollValue = -clampedScrollValue;
|
||||
|
||||
send("wheelReport", { wheelY : invertedScrollValue });
|
||||
|
||||
// Apply blocking delay
|
||||
setBlockWheelEvent(true);
|
||||
setTimeout(() => setBlockWheelEvent(false), blockDelay);
|
||||
send("wheelReport", { wheelY: invertedScrollValue });
|
||||
},
|
||||
[
|
||||
blockDelay,
|
||||
blockWheelEvent,
|
||||
clampMax,
|
||||
clampMin,
|
||||
mouseSensitivity,
|
||||
send,
|
||||
trackpadSensitivity,
|
||||
trackpadThreshold,
|
||||
],
|
||||
[send],
|
||||
);
|
||||
|
||||
const resetMousePosition = useCallback(() => {
|
||||
|
@ -351,11 +327,7 @@ export default function WebRTCVideo() {
|
|||
// which means the Alt Gr key state would then be "stuck". At this
|
||||
// point, we would need to rely on the user to press Alt Gr again
|
||||
// to properly release the state of that modifier.
|
||||
.filter(
|
||||
modifier =>
|
||||
altKey ||
|
||||
(modifier !== modifiers["AltLeft"]),
|
||||
)
|
||||
.filter(modifier => altKey || modifier !== modifiers["AltLeft"])
|
||||
// Meta: Keep if Meta is pressed or if the key isn't a Meta key
|
||||
// Example: If metaKey is true, keep all modifiers
|
||||
// If metaKey is false, filter out 0x08 (MetaLeft) and 0x80 (MetaRight)
|
||||
|
@ -716,7 +688,7 @@ export default function WebRTCVideo() {
|
|||
disablePictureInPicture
|
||||
controlsList="nofullscreen"
|
||||
className={cx(
|
||||
"z-30 max-h-full min-h-[384px] min-w-[512px] max-w-full bg-black/50 object-contain transition-all duration-1000",
|
||||
"z-30 max-h-full min-h-[384px] max-w-full min-w-[512px] bg-black/50 object-contain transition-all duration-1000",
|
||||
{
|
||||
"cursor-none": settings.isCursorHidden,
|
||||
"opacity-0":
|
||||
|
@ -732,7 +704,7 @@ export default function WebRTCVideo() {
|
|||
{peerConnection?.connectionState == "connected" && (
|
||||
<div
|
||||
style={{ animationDuration: "500ms" }}
|
||||
className="pointer-events-none absolute inset-0 flex animate-slideUpFade items-center justify-center"
|
||||
className="animate-slideUpFade pointer-events-none absolute inset-0 flex items-center justify-center"
|
||||
>
|
||||
<div className="relative h-full w-full rounded-md">
|
||||
<LoadingVideoOverlay show={isVideoLoading} />
|
||||
|
|
|
@ -8,14 +8,21 @@ import { GridCard } from "@components/Card";
|
|||
import { TextAreaWithLabel } from "@components/TextArea";
|
||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import { useHidStore, useRTCStore, useUiStore } from "@/hooks/stores";
|
||||
import { chars, keys, modifiers } from "@/keyboardMappings";
|
||||
import { useHidStore, useRTCStore, useUiStore, useSettingsStore } from "@/hooks/stores";
|
||||
import { keys, modifiers } from "@/keyboardMappings";
|
||||
import { layouts, chars } from "@/keyboardLayouts";
|
||||
import notifications from "@/notifications";
|
||||
|
||||
const hidKeyboardPayload = (keys: number[], modifier: number) => {
|
||||
return { keys, modifier };
|
||||
};
|
||||
|
||||
const modifierCode = (shift?: boolean, altRight?: boolean) => {
|
||||
return (shift ? modifiers["ShiftLeft"] : 0)
|
||||
| (altRight ? modifiers["AltRight"] : 0)
|
||||
}
|
||||
const noModifier = 0
|
||||
|
||||
export default function PasteModal() {
|
||||
const TextAreaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const setPasteMode = useHidStore(state => state.setPasteModeEnabled);
|
||||
|
@ -27,6 +34,18 @@ export default function PasteModal() {
|
|||
const [invalidChars, setInvalidChars] = useState<string[]>([]);
|
||||
const close = useClose();
|
||||
|
||||
const keyboardLayout = useSettingsStore(state => state.keyboardLayout);
|
||||
const setKeyboardLayout = useSettingsStore(
|
||||
state => state.setKeyboardLayout,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
send("getKeyboardLayout", {}, resp => {
|
||||
if ("error" in resp) return;
|
||||
setKeyboardLayout(resp.result as string);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const onCancelPasteMode = useCallback(() => {
|
||||
setPasteMode(false);
|
||||
setDisableVideoFocusTrap(false);
|
||||
|
@ -37,27 +56,43 @@ export default function PasteModal() {
|
|||
setPasteMode(false);
|
||||
setDisableVideoFocusTrap(false);
|
||||
if (rpcDataChannel?.readyState !== "open" || !TextAreaRef.current) return;
|
||||
if (!keyboardLayout) return;
|
||||
if (!chars[keyboardLayout]) return;
|
||||
|
||||
const text = TextAreaRef.current.value;
|
||||
|
||||
try {
|
||||
for (const char of text) {
|
||||
const { key, shift } = chars[char] ?? {};
|
||||
const { key, shift, altRight, deadKey, accentKey } = chars[keyboardLayout][char]
|
||||
if (!key) continue;
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
send(
|
||||
"keyboardReport",
|
||||
hidKeyboardPayload([keys[key]], shift ? modifiers["ShiftLeft"] : 0),
|
||||
params => {
|
||||
if ("error" in params) return reject(params.error);
|
||||
send("keyboardReport", hidKeyboardPayload([], 0), params => {
|
||||
const keyz = [ keys[key] ];
|
||||
const modz = [ modifierCode(shift, altRight) ];
|
||||
|
||||
if (deadKey) {
|
||||
keyz.push(keys["Space"]);
|
||||
modz.push(noModifier);
|
||||
}
|
||||
if (accentKey) {
|
||||
keyz.unshift(keys[accentKey.key])
|
||||
modz.unshift(modifierCode(accentKey.shift, accentKey.altRight))
|
||||
}
|
||||
|
||||
for (const [index, kei] of keyz.entries()) {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
send(
|
||||
"keyboardReport",
|
||||
hidKeyboardPayload([kei], modz[index]),
|
||||
params => {
|
||||
if ("error" in params) return reject(params.error);
|
||||
resolve();
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
send("keyboardReport", hidKeyboardPayload([], 0), params => {
|
||||
if ("error" in params) return reject(params.error);
|
||||
resolve();
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
@ -113,7 +148,7 @@ export default function PasteModal() {
|
|||
// @ts-expect-error TS doesn't recognize Intl.Segmenter in some environments
|
||||
[...new Intl.Segmenter().segment(value)]
|
||||
.map(x => x.segment)
|
||||
.filter(char => !chars[char]),
|
||||
.filter(char => !chars[keyboardLayout][char]),
|
||||
),
|
||||
];
|
||||
|
||||
|
@ -132,6 +167,11 @@ export default function PasteModal() {
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<p className="text-xs text-slate-600 dark:text-slate-400">
|
||||
Sending key codes using keyboard layout {layouts[keyboardLayout]}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -302,6 +302,9 @@ interface SettingsState {
|
|||
|
||||
backlightSettings: BacklightSettings;
|
||||
setBacklightSettings: (settings: BacklightSettings) => void;
|
||||
|
||||
keyboardLayout: string;
|
||||
setKeyboardLayout: (layout: string) => void;
|
||||
}
|
||||
|
||||
export const useSettingsStore = create(
|
||||
|
@ -321,8 +324,7 @@ export const useSettingsStore = create(
|
|||
setDeveloperMode: enabled => set({ developerMode: enabled }),
|
||||
|
||||
displayRotation: "270",
|
||||
setDisplayRotation: (rotation: string) =>
|
||||
set({ displayRotation: rotation }),
|
||||
setDisplayRotation: (rotation: string) => set({ displayRotation: rotation }),
|
||||
|
||||
backlightSettings: {
|
||||
max_brightness: 100,
|
||||
|
@ -331,6 +333,9 @@ export const useSettingsStore = create(
|
|||
},
|
||||
setBacklightSettings: (settings: BacklightSettings) =>
|
||||
set({ backlightSettings: settings }),
|
||||
|
||||
keyboardLayout: "en-US",
|
||||
setKeyboardLayout: layout => set({ keyboardLayout: layout }),
|
||||
}),
|
||||
{
|
||||
name: "settings",
|
||||
|
@ -350,67 +355,6 @@ export interface DeviceSettingsState {
|
|||
setScrollSensitivity: (sensitivity: DeviceSettingsState["scrollSensitivity"]) => void;
|
||||
}
|
||||
|
||||
export const useDeviceSettingsStore = create<DeviceSettingsState>(set => ({
|
||||
trackpadSensitivity: 3.0,
|
||||
mouseSensitivity: 5.0,
|
||||
clampMin: -8,
|
||||
clampMax: 8,
|
||||
blockDelay: 25,
|
||||
trackpadThreshold: 10,
|
||||
|
||||
scrollSensitivity: "default",
|
||||
setScrollSensitivity: sensitivity => {
|
||||
const wheelSettings: Record<
|
||||
DeviceSettingsState["scrollSensitivity"],
|
||||
{
|
||||
trackpadSensitivity: DeviceSettingsState["trackpadSensitivity"];
|
||||
mouseSensitivity: DeviceSettingsState["mouseSensitivity"];
|
||||
clampMin: DeviceSettingsState["clampMin"];
|
||||
clampMax: DeviceSettingsState["clampMax"];
|
||||
blockDelay: DeviceSettingsState["blockDelay"];
|
||||
trackpadThreshold: DeviceSettingsState["trackpadThreshold"];
|
||||
}
|
||||
> = {
|
||||
low: {
|
||||
trackpadSensitivity: 2.0,
|
||||
mouseSensitivity: 3.0,
|
||||
clampMin: -6,
|
||||
clampMax: 6,
|
||||
blockDelay: 30,
|
||||
trackpadThreshold: 10,
|
||||
},
|
||||
default: {
|
||||
trackpadSensitivity: 3.0,
|
||||
mouseSensitivity: 5.0,
|
||||
clampMin: -8,
|
||||
clampMax: 8,
|
||||
blockDelay: 25,
|
||||
trackpadThreshold: 10,
|
||||
},
|
||||
high: {
|
||||
trackpadSensitivity: 4.0,
|
||||
mouseSensitivity: 6.0,
|
||||
clampMin: -9,
|
||||
clampMax: 9,
|
||||
blockDelay: 20,
|
||||
trackpadThreshold: 10,
|
||||
},
|
||||
};
|
||||
|
||||
const settings = wheelSettings[sensitivity];
|
||||
|
||||
return set({
|
||||
trackpadSensitivity: settings.trackpadSensitivity,
|
||||
trackpadThreshold: settings.trackpadThreshold,
|
||||
mouseSensitivity: settings.mouseSensitivity,
|
||||
clampMin: settings.clampMin,
|
||||
clampMax: settings.clampMax,
|
||||
blockDelay: settings.blockDelay,
|
||||
scrollSensitivity: sensitivity,
|
||||
});
|
||||
},
|
||||
}));
|
||||
|
||||
export interface RemoteVirtualMediaState {
|
||||
source: "WebRTC" | "HTTP" | "Storage" | null;
|
||||
mode: "CDROM" | "Disk" | null;
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
import { chars as chars_fr_BE, name as name_fr_BE } from "@/keyboardLayouts/fr_BE"
|
||||
import { chars as chars_cs_CZ, name as name_cs_CZ } from "@/keyboardLayouts/cs_CZ"
|
||||
import { chars as chars_en_UK, name as name_en_UK } from "@/keyboardLayouts/en_UK"
|
||||
import { chars as chars_en_US, name as name_en_US } from "@/keyboardLayouts/en_US"
|
||||
import { chars as chars_fr_FR, name as name_fr_FR } from "@/keyboardLayouts/fr_FR"
|
||||
import { chars as chars_de_DE, name as name_de_DE } from "@/keyboardLayouts/de_DE"
|
||||
import { chars as chars_it_IT, name as name_it_IT } from "@/keyboardLayouts/it_IT"
|
||||
import { chars as chars_nb_NO, name as name_nb_NO } from "@/keyboardLayouts/nb_NO"
|
||||
import { chars as chars_es_ES, name as name_es_ES } from "@/keyboardLayouts/es_ES"
|
||||
import { chars as chars_sv_SE, name as name_sv_SE } from "@/keyboardLayouts/sv_SE"
|
||||
import { chars as chars_fr_CH, name as name_fr_CH } from "@/keyboardLayouts/fr_CH"
|
||||
import { chars as chars_de_CH, name as name_de_CH } from "@/keyboardLayouts/de_CH"
|
||||
|
||||
type KeyInfo = { key: string | number; shift?: boolean, altRight?: boolean }
|
||||
export type KeyCombo = KeyInfo & { deadKey?: boolean, accentKey?: KeyInfo }
|
||||
|
||||
export const layouts: Record<string, string> = {
|
||||
be_FR: name_fr_BE,
|
||||
cs_CZ: name_cs_CZ,
|
||||
en_UK: name_en_UK,
|
||||
en_US: name_en_US,
|
||||
fr_FR: name_fr_FR,
|
||||
de_DE: name_de_DE,
|
||||
it_IT: name_it_IT,
|
||||
nb_NO: name_nb_NO,
|
||||
es_ES: name_es_ES,
|
||||
sv_SE: name_sv_SE,
|
||||
fr_CH: name_fr_CH,
|
||||
de_CH: name_de_CH,
|
||||
}
|
||||
|
||||
export const chars: Record<string, Record<string, KeyCombo>> = {
|
||||
be_FR: chars_fr_BE,
|
||||
cs_CZ: chars_cs_CZ,
|
||||
en_UK: chars_en_UK,
|
||||
en_US: chars_en_US,
|
||||
fr_FR: chars_fr_FR,
|
||||
de_DE: chars_de_DE,
|
||||
it_IT: chars_it_IT,
|
||||
nb_NO: chars_nb_NO,
|
||||
es_ES: chars_es_ES,
|
||||
sv_SE: chars_sv_SE,
|
||||
fr_CH: chars_fr_CH,
|
||||
de_CH: chars_de_CH,
|
||||
};
|
|
@ -0,0 +1,244 @@
|
|||
import { KeyCombo } from "../keyboardLayouts"
|
||||
|
||||
export const name = "Čeština";
|
||||
|
||||
const keyTrema = { key: "Backslash" } // tréma (umlaut), two dots placed above a vowel
|
||||
const keyAcute = { key: "Equal" } // accent aigu (acute accent), mark ´ placed above the letter
|
||||
const keyHat = { key: "Digit3", shift: true, altRight: true } // accent circonflexe (accent hat), mark ^ placed above the letter
|
||||
const keyCaron = { key: "Equal", shift: true } // caron or haček (inverted hat), mark ˇ placed above the letter
|
||||
const keyGrave = { key: "Digit7", shift: true, altRight: true } // accent grave, mark ` placed above the letter
|
||||
const keyTilde = { key: "Digit1", shift: true, altRight: true } // tilde, mark ~ placed above the letter
|
||||
const keyRing = { key: "Backquote", shift: true } // kroužek (little ring), mark ° placed above the letter
|
||||
const keyOverdot = { key: "Digit8", shift: true, altRight: true } // overdot (dot above), mark ˙ placed above the letter
|
||||
const keyHook = { key: "Digit6", shift: true, altRight: true } // ogonoek (little hook), mark ˛ placed beneath a letter
|
||||
const keyCedille = { key: "Equal", shift: true, altRight: true } // accent cedille (cedilla), mark ¸ placed beneath a letter
|
||||
|
||||
export const chars = {
|
||||
A: { key: "KeyA", shift: true },
|
||||
"Ä": { key: "KeyA", shift: true, accentKey: keyTrema },
|
||||
"Á": { key: "KeyA", shift: true, accentKey: keyAcute },
|
||||
"Â": { key: "KeyA", shift: true, accentKey: keyHat },
|
||||
"À": { key: "KeyA", shift: true, accentKey: keyGrave },
|
||||
"Ã": { key: "KeyA", shift: true, accentKey: keyTilde },
|
||||
"Ȧ": { key: "KeyA", shift: true, accentKey: keyOverdot },
|
||||
"Ą": { key: "KeyA", shift: true, accentKey: keyHook },
|
||||
B: { key: "KeyB", shift: true },
|
||||
"Ḃ": { key: "KeyB", shift: true, accentKEy: keyOverdot },
|
||||
C: { key: "KeyC", shift: true },
|
||||
"Č": { key: "KeyC", shift: true, accentKey: keyCaron },
|
||||
"Ċ": { key: "KeyC", shift: true, accentKey: keyOverdot },
|
||||
"Ç": { key: "KeyC", shift: true, accentKey: keyCedille },
|
||||
D: { key: "KeyD", shift: true },
|
||||
"Ď": { key: "KeyD", shift: true, accentKey: keyCaron },
|
||||
"Ḋ": { key: "KeyD", shift: true, accentKey: keyOverdot },
|
||||
E: { key: "KeyE", shift: true },
|
||||
"Ë": { key: "KeyE", shift: true, accentKey: keyTrema },
|
||||
"É": { key: "KeyE", shift: true, accentKey: keyAcute },
|
||||
"Ê": { key: "KeyE", shift: true, accentKey: keyHat },
|
||||
"Ě": { key: "KeyE", shift: true, accentKey: keyCaron },
|
||||
"È": { key: "KeyE", shift: true, accentKey: keyGrave },
|
||||
"Ẽ": { key: "KeyE", shift: true, accentKey: keyTilde },
|
||||
"Ė": { key: "KeyE", shift: true, accentKEy: keyOverdot },
|
||||
"Ę": { key: "KeyE", shift: true, accentKey: keyHook },
|
||||
F: { key: "KeyF", shift: true },
|
||||
"Ḟ": { key: "KeyF", shift: true, accentKey: keyOverdot },
|
||||
G: { key: "KeyG", shift: true },
|
||||
"Ġ": { key: "KeyG", shift: true, accentKey: keyOverdot },
|
||||
H: { key: "KeyH", shift: true },
|
||||
"Ḣ": { key: "KeyH", shift: true, accentKey: keyOverdot },
|
||||
I: { key: "KeyI", shift: true },
|
||||
"Ï": { key: "KeyI", shift: true, accentKey: keyTrema },
|
||||
"Í": { key: "KeyI", shift: true, accentKey: keyAcute },
|
||||
"Î": { key: "KeyI", shift: true, accentKey: keyHat },
|
||||
"Ì": { key: "KeyI", shift: true, accentKey: keyGrave },
|
||||
"Ĩ": { key: "KeyI", shift: true, accentKey: keyTilde },
|
||||
"İ": { key: "KeyI", shift: true, accentKey: keyOverdot },
|
||||
"Į": { key: "KeyI", shift: true, accentKey: keyHook },
|
||||
J: { key: "KeyJ", shift: true },
|
||||
K: { key: "KeyK", shift: true },
|
||||
L: { key: "KeyL", shift: true },
|
||||
"Ŀ": { key: "KeyL", shift: true },
|
||||
M: { key: "KeyM", shift: true },
|
||||
"Ṁ": { key: "KeyM", shift: true },
|
||||
N: { key: "KeyN", shift: true },
|
||||
"Ň": { key: "KeyN", shift: true, accentKey: keyCaron },
|
||||
"Ñ": { key: "KeyN", shift: true, accentKey: keyTilde },
|
||||
"Ṅ": { key: "KeyN", shift: true, accentKEy: keyOverdot },
|
||||
O: { key: "KeyO", shift: true },
|
||||
"Ö": { key: "KeyO", shift: true, accentKey: keyTrema },
|
||||
"Ó": { key: "KeyO", shift: true, accentKey: keyAcute },
|
||||
"Ô": { key: "KeyO", shift: true, accentKey: keyHat },
|
||||
"Ò": { key: "KeyO", shift: true, accentKey: keyGrave },
|
||||
"Õ": { key: "KeyO", shift: true, accentKey: keyTilde },
|
||||
"Ȯ": { key: "KeyO", shift: true, accentKey: keyOverdot },
|
||||
"Ǫ": { key: "KeyO", shift: true, accentKey: keyHook },
|
||||
P: { key: "KeyP", shift: true },
|
||||
"Ṗ": { key: "KeyP", shift: true, accentKey: keyOverdot },
|
||||
Q: { key: "KeyQ", shift: true },
|
||||
R: { key: "KeyR", shift: true },
|
||||
"Ř": { key: "KeyR", shift: true, accentKey: keyCaron },
|
||||
"Ṙ": { key: "KeyR", shift: true, accentKey: keyOverdot },
|
||||
S: { key: "KeyS", shift: true },
|
||||
"Š": { key: "KeyS", shift: true, accentKey: keyCaron },
|
||||
"Ṡ": { key: "KeyS", shift: true, accentKey: keyOverdot },
|
||||
T: { key: "KeyT", shift: true },
|
||||
"Ť": { key: "KeyT", shift: true, accentKey: keyCaron },
|
||||
"Ṫ": { key: "KeyT", shift: true, accentKey: keyOverdot },
|
||||
U: { key: "KeyU", shift: true },
|
||||
"Ü": { key: "KeyU", shift: true, accentKey: keyTrema },
|
||||
"Ú": { key: "KeyU", shift: true, accentKey: keyAcute },
|
||||
"Û": { key: "KeyU", shift: true, accentKey: keyHat },
|
||||
"Ù": { key: "KeyU", shift: true, accentKey: keyGrave },
|
||||
"Ũ": { key: "KeyU", shift: true, accentKey: keyTilde },
|
||||
"Ů": { key: "KeyU", shift: true, accentKey: keyRing },
|
||||
"Ų": { key: "KeyU", shift: true, accentKey: keyHook },
|
||||
V: { key: "KeyV", shift: true },
|
||||
W: { key: "KeyW", shift: true },
|
||||
"Ẇ": { key: "KeyW", shift: true, accentKey: keyOverdot },
|
||||
X: { key: "KeyX", shift: true },
|
||||
"Ẋ": { key: "KeyX", shift: true, accentKey: keyOverdot },
|
||||
Y: { key: "KeyY", shift: true },
|
||||
"Ý": { key: "KeyY", shift: true, accentKey: keyAcute },
|
||||
"Ẏ": { key: "KeyY", shift: true, accentKey: keyOverdot },
|
||||
Z: { key: "KeyZ", shift: true },
|
||||
"Ż": { key: "KeyZ", shift: true, accentKey: keyOverdot },
|
||||
a: { key: "KeyA" },
|
||||
"ä": { key: "KeyA", accentKey: keyTrema },
|
||||
"â": { key: "KeyA", accentKey: keyHat },
|
||||
"à": { key: "KeyA", accentKey: keyGrave },
|
||||
"ã": { key: "KeyA", accentKey: keyTilde },
|
||||
"ȧ": { key: "KeyA", accentKey: keyOverdot },
|
||||
"ą": { key: "KeyA", accentKey: keyHook },
|
||||
b: { key: "KeyB" },
|
||||
"{": { key: "KeyB", altRight: true },
|
||||
"ḃ": { key: "KeyB", accentKey: keyOverdot },
|
||||
c: { key: "KeyC" },
|
||||
"&": { key: "KeyC", altRight: true },
|
||||
"ç": { key: "KeyC", accentKey: keyCedille },
|
||||
"ċ": { key: "KeyC", accentKey: keyOverdot },
|
||||
d: { key: "KeyD" },
|
||||
"ď": { key: "KeyD", accentKey: keyCaron },
|
||||
"ḋ": { key: "KeyD", accentKey: keyOverdot },
|
||||
"Đ": { key: "KeyD", altRight: true },
|
||||
e: { key: "KeyE" },
|
||||
"ë": { key: "KeyE", accentKey: keyTrema },
|
||||
"ê": { key: "KeyE", accentKey: keyHat },
|
||||
"ẽ": { key: "KeyE", accentKey: keyTilde },
|
||||
"è": { key: "KeyE", accentKey: keyGrave },
|
||||
"ė": { key: "KeyE", accentKey: keyOverdot },
|
||||
"ę": { key: "KeyE", accentKey: keyHook },
|
||||
"€": { key: "KeyE", altRight: true },
|
||||
f: { key: "KeyF" },
|
||||
"ḟ": { key: "KeyF", accentKey: keyOverdot },
|
||||
"[": { key: "KeyF", altRight: true },
|
||||
g: { key: "KeyG" },
|
||||
"ġ": { key: "KeyG", accentKey: keyOverdot },
|
||||
"]": { key: "KeyF", altRight: true },
|
||||
h: { key: "KeyH" },
|
||||
"ḣ": { key: "KeyH", accentKey: keyOverdot },
|
||||
i: { key: "KeyI" },
|
||||
"ï": { key: "KeyI", accentKey: keyTrema },
|
||||
"î": { key: "KeyI", accentKey: keyHat },
|
||||
"ì": { key: "KeyI", accentKey: keyGrave },
|
||||
"ĩ": { key: "KeyI", accentKey: keyTilde },
|
||||
"ı": { key: "KeyI", accentKey: keyOverdot },
|
||||
"į": { key: "KeyI", accentKey: keyHook },
|
||||
j: { key: "KeyJ" },
|
||||
"ȷ": { key: "KeyJ", accentKey: keyOverdot },
|
||||
k: { key: "KeyK" },
|
||||
"ł": { key: "KeyK", altRight: true },
|
||||
l: { key: "KeyL" },
|
||||
"ŀ": { key: "KeyL", accentKey: keyOverdot },
|
||||
"Ł": { key: "KeyL", altRight: true },
|
||||
m: { key: "KeyM" },
|
||||
"ṁ": { key: "KeyM", accentKey: keyOverdot },
|
||||
n: { key: "KeyN" },
|
||||
"}": { key: "KeyN", altRight: true },
|
||||
"ň": { key: "KeyN", accentKey: keyCaron },
|
||||
"ñ": { key: "KeyN", accentKey: keyTilde },
|
||||
"ṅ": { key: "KeyN", accentKey: keyOverdot },
|
||||
o: { key: "KeyO" },
|
||||
"ö": { key: "Key0", accentKey: keyTrema },
|
||||
"ó": { key: "KeyO", accentKey: keyAcute },
|
||||
"ô": { key: "KeyO", accentKey: keyHat },
|
||||
"ò": { key: "KeyO", accentKey: keyGrave },
|
||||
"õ": { key: "KeyO", accentKey: keyTilde },
|
||||
"ȯ": { key: "KeyO", accentKey: keyOverdot },
|
||||
"ǫ": { key: "KeyO", accentKey: keyHook },
|
||||
p: { key: "KeyP" },
|
||||
"ṗ": { key: "KeyP", accentKey: keyOverdot },
|
||||
q: { key: "KeyQ" },
|
||||
r: { key: "KeyR" },
|
||||
"ṙ": { key: "KeyR", accentKey: keyOverdot },
|
||||
s: { key: "KeyS" },
|
||||
"ṡ": { key: "KeyS", accentKey: keyOverdot },
|
||||
"đ": { key: "KeyS", altRight: true },
|
||||
t: { key: "KeyT" },
|
||||
"ť": { key: "KeyT", accentKey: keyCaron },
|
||||
"ṫ": { key: "KeyT", accentKey: keyOverdot },
|
||||
u: { key: "KeyU" },
|
||||
"ü": { key: "KeyU", accentKey: keyTrema },
|
||||
"û": { key: "KeyU", accentKey: keyHat },
|
||||
"ù": { key: "KeyU", accentKey: keyGrave },
|
||||
"ũ": { key: "KeyU", accentKey: keyTilde },
|
||||
"ų": { key: "KeyU", accentKey: keyHook },
|
||||
v: { key: "KeyV" },
|
||||
"@": { key: "KeyV", altRight: true },
|
||||
w: { key: "KeyW" },
|
||||
"ẇ": { key: "KeyW", accentKey: keyOverdot },
|
||||
x: { key: "KeyX" },
|
||||
"#": { key: "KeyX", altRight: true },
|
||||
"ẋ": { key: "KeyX", accentKey: keyOverdot },
|
||||
y: { key: "KeyY" },
|
||||
"ẏ": { key: "KeyY", accentKey: keyOverdot },
|
||||
z: { key: "KeyZ" },
|
||||
"ż": { key: "KeyZ", accentKey: keyOverdot },
|
||||
";": { key: "Backquote" },
|
||||
"°": { key: "Backquote", shift: true, deadKey: true },
|
||||
"+": { key: "Digit1" },
|
||||
1: { key: "Digit1", shift: true },
|
||||
"ě": { key: "Digit2" },
|
||||
2: { key: "Digit2", shift: true },
|
||||
"š": { key: "Digit3" },
|
||||
3: { key: "Digit3", shift: true },
|
||||
"č": { key: "Digit4" },
|
||||
4: { key: "Digit4", shift: true },
|
||||
"ř": { key: "Digit5" },
|
||||
5: { key: "Digit5", shift: true },
|
||||
"ž": { key: "Digit6" },
|
||||
6: { key: "Digit6", shift: true },
|
||||
"ý": { key: "Digit7" },
|
||||
7: { key: "Digit7", shift: true },
|
||||
"á": { key: "Digit8" },
|
||||
8: { key: "Digit8", shift: true },
|
||||
"í": { key: "Digit9" },
|
||||
9: { key: "Digit9", shift: true },
|
||||
"é": { key: "Digit0" },
|
||||
0: { key: "Digit0", shift: true },
|
||||
"=": { key: "Minus" },
|
||||
"%": { key: "Minus", shift: true },
|
||||
"ú": { key: "BracketLeft" },
|
||||
"/": { key: "BracketLeft", shift: true },
|
||||
")": { key: "BracketRight" },
|
||||
"(": { key: "BracketRight", shift: true },
|
||||
"ů": { key: "Semicolon" },
|
||||
"\"": { key: "Semicolon", shift: true },
|
||||
"§": { key: "Quote" },
|
||||
"!": { key: "Quote", shift: true },
|
||||
"'": { key: "Backslash", shift: true },
|
||||
",": { key: "Comma" },
|
||||
"?": { key: "Comma", shift: true },
|
||||
"<": { key: "Comma", altRight: true },
|
||||
".": { key: "Period" },
|
||||
":": { key: "Period", shift: true },
|
||||
">": { key: "Period", altRight: true },
|
||||
"-": { key: "Slash" },
|
||||
"_": { key: "Slash", shift: true },
|
||||
"*": { key: "Slash", altRight: true },
|
||||
"\\": { key: "IntlBackslash" },
|
||||
"|": { key: "IntlBackslash", shift: true },
|
||||
" ": { key: "Space" },
|
||||
"\n": { key: "Enter" },
|
||||
Enter: { key: "Enter" },
|
||||
Tab: { key: "Tab" },
|
||||
} as Record<string, KeyCombo>;
|
|
@ -0,0 +1,165 @@
|
|||
import { KeyCombo } from "../keyboardLayouts"
|
||||
|
||||
export const name = "Schwiizerdütsch";
|
||||
|
||||
const keyTrema = { key: "BracketRight" } // tréma (umlaut), two dots placed above a vowel
|
||||
const keyAcute = { key: "Minus", altRight: true } // accent aigu (acute accent), mark ´ placed above the letter
|
||||
const keyHat = { key: "Equal" } // accent circonflexe (accent hat), mark ^ placed above the letter
|
||||
const keyGrave = { key: "Equal", shift: true } // accent grave, mark ` placed above the letter
|
||||
const keyTilde = { key: "Equal", altRight: true } // tilde, mark ~ placed above the letter
|
||||
|
||||
export const chars = {
|
||||
A: { key: "KeyA", shift: true },
|
||||
"Ä": { key: "KeyA", shift: true, accentKey: keyTrema },
|
||||
"Á": { key: "KeyA", shift: true, accentKey: keyAcute },
|
||||
"Â": { key: "KeyA", shift: true, accentKey: keyHat },
|
||||
"À": { key: "KeyA", shift: true, accentKey: keyGrave },
|
||||
"Ã": { key: "KeyA", shift: true, accentKey: keyTilde },
|
||||
B: { key: "KeyB", shift: true },
|
||||
C: { key: "KeyC", shift: true },
|
||||
D: { key: "KeyD", shift: true },
|
||||
E: { key: "KeyE", shift: true },
|
||||
"Ë": { key: "KeyE", shift: true, accentKey: keyTrema },
|
||||
"É": { key: "KeyE", shift: true, accentKey: keyAcute },
|
||||
"Ê": { key: "KeyE", shift: true, accentKey: keyHat },
|
||||
"È": { key: "KeyE", shift: true, accentKey: keyGrave },
|
||||
"Ẽ": { key: "KeyE", shift: true, accentKey: keyTilde },
|
||||
F: { key: "KeyF", shift: true },
|
||||
G: { key: "KeyG", shift: true },
|
||||
H: { key: "KeyH", shift: true },
|
||||
I: { key: "KeyI", shift: true },
|
||||
"Ï": { key: "KeyI", shift: true, accentKey: keyTrema },
|
||||
"Í": { key: "KeyI", shift: true, accentKey: keyAcute },
|
||||
"Î": { key: "KeyI", shift: true, accentKey: keyHat },
|
||||
"Ì": { key: "KeyI", shift: true, accentKey: keyGrave },
|
||||
"Ĩ": { key: "KeyI", shift: true, accentKey: keyTilde },
|
||||
J: { key: "KeyJ", shift: true },
|
||||
K: { key: "KeyK", shift: true },
|
||||
L: { key: "KeyL", shift: true },
|
||||
M: { key: "KeyM", shift: true },
|
||||
N: { key: "KeyN", shift: true },
|
||||
O: { key: "KeyO", shift: true },
|
||||
"Ö": { key: "KeyO", shift: true, accentKey: keyTrema },
|
||||
"Ó": { key: "KeyO", shift: true, accentKey: keyAcute },
|
||||
"Ô": { key: "KeyO", shift: true, accentKey: keyHat },
|
||||
"Ò": { key: "KeyO", shift: true, accentKey: keyGrave },
|
||||
"Õ": { key: "KeyO", shift: true, accentKey: keyTilde },
|
||||
P: { key: "KeyP", shift: true },
|
||||
Q: { key: "KeyQ", shift: true },
|
||||
R: { key: "KeyR", shift: true },
|
||||
S: { key: "KeyS", shift: true },
|
||||
T: { key: "KeyT", shift: true },
|
||||
U: { key: "KeyU", shift: true },
|
||||
"Ü": { key: "KeyU", shift: true, accentKey: keyTrema },
|
||||
"Ú": { key: "KeyU", shift: true, accentKey: keyAcute },
|
||||
"Û": { key: "KeyU", shift: true, accentKey: keyHat },
|
||||
"Ù": { key: "KeyU", shift: true, accentKey: keyGrave },
|
||||
"Ũ": { key: "KeyU", shift: true, accentKey: keyTilde },
|
||||
V: { key: "KeyV", shift: true },
|
||||
W: { key: "KeyW", shift: true },
|
||||
X: { key: "KeyX", shift: true },
|
||||
Y: { key: "KeyZ", shift: true },
|
||||
Z: { key: "KeyY", shift: true },
|
||||
a: { key: "KeyA" },
|
||||
"á": { key: "KeyA", accentKey: keyAcute },
|
||||
"â": { key: "KeyA", accentKey: keyHat },
|
||||
"ã": { key: "KeyA", accentKey: keyTilde },
|
||||
b: { key: "KeyB" },
|
||||
c: { key: "KeyC" },
|
||||
d: { key: "KeyD" },
|
||||
e: { key: "KeyE" },
|
||||
"ë": { key: "KeyE", accentKey: keyTrema },
|
||||
"ê": { key: "KeyE", accentKey: keyHat },
|
||||
"ẽ": { key: "KeyE", accentKey: keyTilde },
|
||||
"€": { key: "KeyE", altRight: true },
|
||||
f: { key: "KeyF" },
|
||||
g: { key: "KeyG" },
|
||||
h: { key: "KeyH" },
|
||||
i: { key: "KeyI" },
|
||||
"ï": { key: "KeyI", accentKey: keyTrema },
|
||||
"í": { key: "KeyI", accentKey: keyAcute },
|
||||
"î": { key: "KeyI", accentKey: keyHat },
|
||||
"ì": { key: "KeyI", accentKey: keyGrave },
|
||||
"ĩ": { key: "KeyI", accentKey: keyTilde },
|
||||
j: { key: "KeyJ" },
|
||||
k: { key: "KeyK" },
|
||||
l: { key: "KeyL" },
|
||||
m: { key: "KeyM" },
|
||||
n: { key: "KeyN" },
|
||||
o: { key: "KeyO" },
|
||||
"ó": { key: "KeyO", accentKey: keyAcute },
|
||||
"ô": { key: "KeyO", accentKey: keyHat },
|
||||
"ò": { key: "KeyO", accentKey: keyGrave },
|
||||
"õ": { key: "KeyO", accentKey: keyTilde },
|
||||
p: { key: "KeyP" },
|
||||
q: { key: "KeyQ" },
|
||||
r: { key: "KeyR" },
|
||||
s: { key: "KeyS" },
|
||||
t: { key: "KeyT" },
|
||||
u: { key: "KeyU" },
|
||||
"ú": { key: "KeyU", accentKey: keyAcute },
|
||||
"û": { key: "KeyU", accentKey: keyHat },
|
||||
"ù": { key: "KeyU", accentKey: keyGrave },
|
||||
"ũ": { key: "KeyU", accentKey: keyTilde },
|
||||
v: { key: "KeyV" },
|
||||
w: { key: "KeyW" },
|
||||
x: { key: "KeyX" },
|
||||
y: { key: "KeyZ" },
|
||||
z: { key: "KeyY" },
|
||||
"§": { key: "Backquote" },
|
||||
"°": { key: "Backquote", shift: true },
|
||||
1: { key: "Digit1" },
|
||||
"+": { key: "Digit1", shift: true },
|
||||
"|": { key: "Digit1", altRight: true },
|
||||
2: { key: "Digit2" },
|
||||
"\"": { key: "Digit2", shift: true },
|
||||
"@": { key: "Digit2", altRight: true },
|
||||
3: { key: "Digit3" },
|
||||
"*": { key: "Digit3", shift: true },
|
||||
"#": { key: "Digit3", altRight: true },
|
||||
4: { key: "Digit4" },
|
||||
"ç": { key: "Digit4", shift: true },
|
||||
5: { key: "Digit5" },
|
||||
"%": { key: "Digit5", shift: true },
|
||||
6: { key: "Digit6" },
|
||||
"&": { key: "Digit6", shift: true },
|
||||
7: { key: "Digit7" },
|
||||
"/": { key: "Digit7", shift: true },
|
||||
8: { key: "Digit8" },
|
||||
"(": { key: "Digit8", shift: true },
|
||||
9: { key: "Digit9" },
|
||||
")": { key: "Digit9", shift: true },
|
||||
0: { key: "Digit0" },
|
||||
"=": { key: "Digit0", shift: true },
|
||||
"'": { key: "Minus" },
|
||||
"?": { key: "Minus", shift: true },
|
||||
"^": { key: "Equal", deadKey: true },
|
||||
"`": { key: "Equal", shift: true },
|
||||
"~": { key: "Equal", altRight: true, deadKey: true },
|
||||
"ü": { key: "BracketLeft" },
|
||||
"è": { key: "BracketLeft", shift: true },
|
||||
"[": { key: "BracketLeft", altRight: true },
|
||||
"!": { key: "BracketRight", shift: true },
|
||||
"]": { key: "BracketRight", altRight: true },
|
||||
"ö": { key: "Semicolon" },
|
||||
"é": { key: "Semicolon", shift: true },
|
||||
"ä": { key: "Quote" },
|
||||
"à": { key: "Quote", shift: true },
|
||||
"{": { key: "Quote", altRight: true },
|
||||
"$": { key: "Backslash" },
|
||||
"£": { key: "Backslash", shift: true },
|
||||
"}": { key: "Backslash", altRight: true },
|
||||
",": { key: "Comma" },
|
||||
";": { key: "Comma", shift: true },
|
||||
".": { key: "Period" },
|
||||
":": { key: "Period", shift: true },
|
||||
"-": { key: "Slash" },
|
||||
"_": { key: "Slash", shift: true },
|
||||
"<": { key: "IntlBackslash" },
|
||||
">": { key: "IntlBackslash", shift: true },
|
||||
"\\": { key: "IntlBackslash", altRight: true },
|
||||
" ": { key: "Space" },
|
||||
"\n": { key: "Enter" },
|
||||
Enter: { key: "Enter" },
|
||||
Tab: { key: "Tab" },
|
||||
} as Record<string, KeyCombo>;
|
|
@ -0,0 +1,152 @@
|
|||
import { KeyCombo } from "../keyboardLayouts"
|
||||
|
||||
export const name = "Deutsch";
|
||||
|
||||
const keyAcute = { key: "Equal" } // accent aigu (acute accent), mark ´ placed above the letter
|
||||
const keyHat = { key: "Backquote" } // accent circonflexe (accent hat), mark ^ placed above the letter
|
||||
const keyGrave = { key: "Equal", shift: true } // accent grave, mark ` placed above the letter
|
||||
|
||||
export const chars = {
|
||||
A: { key: "KeyA", shift: true },
|
||||
"Á": { key: "KeyA", shift: true, accentKey: keyAcute },
|
||||
"Â": { key: "KeyA", shift: true, accentKey: keyHat },
|
||||
"À": { key: "KeyA", shift: true, accentKey: keyGrave },
|
||||
B: { key: "KeyB", shift: true },
|
||||
C: { key: "KeyC", shift: true },
|
||||
D: { key: "KeyD", shift: true },
|
||||
E: { key: "KeyE", shift: true },
|
||||
"É": { key: "KeyE", shift: true, accentKey: keyAcute },
|
||||
"Ê": { key: "KeyE", shift: true, accentKey: keyHat },
|
||||
"È": { key: "KeyE", shift: true, accentKey: keyGrave },
|
||||
F: { key: "KeyF", shift: true },
|
||||
G: { key: "KeyG", shift: true },
|
||||
H: { key: "KeyH", shift: true },
|
||||
I: { key: "KeyI", shift: true },
|
||||
"Í": { key: "KeyI", shift: true, accentKey: keyAcute },
|
||||
"Î": { key: "KeyI", shift: true, accentKey: keyHat },
|
||||
"Ì": { key: "KeyI", shift: true, accentKey: keyGrave },
|
||||
J: { key: "KeyJ", shift: true },
|
||||
K: { key: "KeyK", shift: true },
|
||||
L: { key: "KeyL", shift: true },
|
||||
M: { key: "KeyM", shift: true },
|
||||
N: { key: "KeyN", shift: true },
|
||||
O: { key: "KeyO", shift: true },
|
||||
"Ó": { key: "KeyO", shift: true, accentKey: keyAcute },
|
||||
"Ô": { key: "KeyO", shift: true, accentKey: keyHat },
|
||||
"Ò": { key: "KeyO", shift: true, accentKey: keyGrave },
|
||||
P: { key: "KeyP", shift: true },
|
||||
Q: { key: "KeyQ", shift: true },
|
||||
R: { key: "KeyR", shift: true },
|
||||
S: { key: "KeyS", shift: true },
|
||||
T: { key: "KeyT", shift: true },
|
||||
U: { key: "KeyU", shift: true },
|
||||
"Ú": { key: "KeyU", shift: true, accentKey: keyAcute },
|
||||
"Û": { key: "KeyU", shift: true, accentKey: keyHat },
|
||||
"Ù": { key: "KeyU", shift: true, accentKey: keyGrave },
|
||||
V: { key: "KeyV", shift: true },
|
||||
W: { key: "KeyW", shift: true },
|
||||
X: { key: "KeyX", shift: true },
|
||||
Y: { key: "KeyZ", shift: true },
|
||||
Z: { key: "KeyY", shift: true },
|
||||
a: { key: "KeyA" },
|
||||
"á": { key: "KeyA", accentKey: keyAcute },
|
||||
"â": { key: "KeyA", accentKey: keyHat },
|
||||
"à": { key: "KeyA", accentKey: keyGrave},
|
||||
b: { key: "KeyB" },
|
||||
c: { key: "KeyC" },
|
||||
d: { key: "KeyD" },
|
||||
e: { key: "KeyE" },
|
||||
"é": { key: "KeyE", accentKey: keyAcute},
|
||||
"ê": { key: "KeyE", accentKey: keyHat },
|
||||
"è": { key: "KeyE", accentKey: keyGrave },
|
||||
"€": { key: "KeyE", altRight: true },
|
||||
f: { key: "KeyF" },
|
||||
g: { key: "KeyG" },
|
||||
h: { key: "KeyH" },
|
||||
i: { key: "KeyI" },
|
||||
"í": { key: "KeyI", accentKey: keyAcute },
|
||||
"î": { key: "KeyI", accentKey: keyHat },
|
||||
"ì": { key: "KeyI", accentKey: keyGrave },
|
||||
j: { key: "KeyJ" },
|
||||
k: { key: "KeyK" },
|
||||
l: { key: "KeyL" },
|
||||
m: { key: "KeyM" },
|
||||
"µ": { key: "KeyM", altRight: true },
|
||||
n: { key: "KeyN" },
|
||||
o: { key: "KeyO" },
|
||||
"ó": { key: "KeyO", accentKey: keyAcute },
|
||||
"ô": { key: "KeyO", accentKey: keyHat },
|
||||
"ò": { key: "KeyO", accentKey: keyGrave },
|
||||
p: { key: "KeyP" },
|
||||
q: { key: "KeyQ" },
|
||||
"@": { key: "KeyQ", altRight: true },
|
||||
r: { key: "KeyR" },
|
||||
s: { key: "KeyS" },
|
||||
t: { key: "KeyT" },
|
||||
u: { key: "KeyU" },
|
||||
"ú": { key: "KeyU", accentKey: keyAcute },
|
||||
"û": { key: "KeyU", accentKey: keyHat },
|
||||
"ù": { key: "KeyU", accentKey: keyGrave },
|
||||
v: { key: "KeyV" },
|
||||
w: { key: "KeyW" },
|
||||
x: { key: "KeyX" },
|
||||
y: { key: "KeyZ" },
|
||||
z: { key: "KeyY" },
|
||||
"°": { key: "Backquote", shift: true },
|
||||
"^": { key: "Backquote", deadKey: true },
|
||||
1: { key: "Digit1" },
|
||||
"!": { key: "Digit1", shift: true },
|
||||
2: { key: "Digit2" },
|
||||
"\"": { key: "Digit2", shift: true },
|
||||
"²": { key: "Digit2", altRight: true },
|
||||
3: { key: "Digit3" },
|
||||
"§": { key: "Digit3", shift: true },
|
||||
"³": { key: "Digit3", altRight: true },
|
||||
4: { key: "Digit4" },
|
||||
"$": { key: "Digit4", shift: true },
|
||||
5: { key: "Digit5" },
|
||||
"%": { key: "Digit5", shift: true },
|
||||
6: { key: "Digit6" },
|
||||
"&": { key: "Digit6", shift: true },
|
||||
7: { key: "Digit7" },
|
||||
"/": { key: "Digit7", shift: true },
|
||||
"{": { key: "Digit7", altRight: true },
|
||||
8: { key: "Digit8" },
|
||||
"(": { key: "Digit8", shift: true },
|
||||
"[": { key: "Digit8", altRight: true },
|
||||
9: { key: "Digit9" },
|
||||
")": { key: "Digit9", shift: true },
|
||||
"]": { key: "Digit9", altRight: true },
|
||||
0: { key: "Digit0" },
|
||||
"=": { key: "Digit0", shift: true },
|
||||
"}": { key: "Digit0", altRight: true },
|
||||
"ß": { key: "Minus" },
|
||||
"?": { key: "Minus", shift: true },
|
||||
"\\": { key: "Minus", altRight: true },
|
||||
"´": { key: "Equal", deadKey: true },
|
||||
"`": { key: "Equal", shift: true, deadKey: true },
|
||||
"ü": { key: "BracketLeft" },
|
||||
"Ü": { key: "BracketLeft", shift: true },
|
||||
"+": { key: "BracketRight" },
|
||||
"*": { key: "BracketRight", shift: true },
|
||||
"~": { key: "BracketRight", altRight: true },
|
||||
"ö": { key: "Semicolon" },
|
||||
"Ö": { key: "Semicolon", shift: true },
|
||||
"ä": { key: "Quote" },
|
||||
"Ä": { key: "Quote", shift: true },
|
||||
"#": { key: "Backslash" },
|
||||
"'": { key: "Backslash", shift: true },
|
||||
",": { key: "Comma" },
|
||||
";": { key: "Comma", shift: true },
|
||||
".": { key: "Period" },
|
||||
":": { key: "Period", shift: true },
|
||||
"-": { key: "Slash" },
|
||||
"_": { key: "Slash", shift: true },
|
||||
"<": { key: "IntlBackslash" },
|
||||
">": { key: "IntlBackslash", shift: true },
|
||||
"|": { key: "IntlBackslash", altRight: true },
|
||||
" ": { key: "Space" },
|
||||
"\n": { key: "Enter" },
|
||||
Enter: { key: "Enter" },
|
||||
Tab: { key: "Tab" },
|
||||
} as Record<string, KeyCombo>;
|
|
@ -0,0 +1,107 @@
|
|||
import { KeyCombo } from "../keyboardLayouts"
|
||||
|
||||
export const name = "English (UK)";
|
||||
|
||||
export const chars = {
|
||||
A: { key: "KeyA", shift: true },
|
||||
B: { key: "KeyB", shift: true },
|
||||
C: { key: "KeyC", shift: true },
|
||||
D: { key: "KeyD", shift: true },
|
||||
E: { key: "KeyE", shift: true },
|
||||
F: { key: "KeyF", shift: true },
|
||||
G: { key: "KeyG", shift: true },
|
||||
H: { key: "KeyH", shift: true },
|
||||
I: { key: "KeyI", shift: true },
|
||||
J: { key: "KeyJ", shift: true },
|
||||
K: { key: "KeyK", shift: true },
|
||||
L: { key: "KeyL", shift: true },
|
||||
M: { key: "KeyM", shift: true },
|
||||
N: { key: "KeyN", shift: true },
|
||||
O: { key: "KeyO", shift: true },
|
||||
P: { key: "KeyP", shift: true },
|
||||
Q: { key: "KeyQ", shift: true },
|
||||
R: { key: "KeyR", shift: true },
|
||||
S: { key: "KeyS", shift: true },
|
||||
T: { key: "KeyT", shift: true },
|
||||
U: { key: "KeyU", shift: true },
|
||||
V: { key: "KeyV", shift: true },
|
||||
W: { key: "KeyW", shift: true },
|
||||
X: { key: "KeyX", shift: true },
|
||||
Y: { key: "KeyY", shift: true },
|
||||
Z: { key: "KeyZ", shift: true },
|
||||
a: { key: "KeyA" },
|
||||
b: { key: "KeyB" },
|
||||
c: { key: "KeyC" },
|
||||
d: { key: "KeyD" },
|
||||
e: { key: "KeyE" },
|
||||
f: { key: "KeyF" },
|
||||
g: { key: "KeyG" },
|
||||
h: { key: "KeyH" },
|
||||
i: { key: "KeyI" },
|
||||
j: { key: "KeyJ" },
|
||||
k: { key: "KeyK" },
|
||||
l: { key: "KeyL" },
|
||||
m: { key: "KeyM" },
|
||||
n: { key: "KeyN" },
|
||||
o: { key: "KeyO" },
|
||||
p: { key: "KeyP" },
|
||||
q: { key: "KeyQ" },
|
||||
r: { key: "KeyR" },
|
||||
s: { key: "KeyS" },
|
||||
t: { key: "KeyT" },
|
||||
u: { key: "KeyU" },
|
||||
v: { key: "KeyV" },
|
||||
w: { key: "KeyW" },
|
||||
x: { key: "KeyX" },
|
||||
y: { key: "KeyY" },
|
||||
z: { key: "KeyZ" },
|
||||
1: { key: "Digit1" },
|
||||
"!": { key: "Digit1", shift: true },
|
||||
2: { key: "Digit2" },
|
||||
"\"": { key: "Digit2", shift: true },
|
||||
3: { key: "Digit3" },
|
||||
"£": { key: "Digit3", shift: true },
|
||||
4: { key: "Digit4" },
|
||||
$: { key: "Digit4", shift: true },
|
||||
"€": { key: "Digit4", altRight: true },
|
||||
5: { key: "Digit5" },
|
||||
"%": { key: "Digit5", shift: true },
|
||||
6: { key: "Digit6" },
|
||||
"^": { key: "Digit6", shift: true },
|
||||
7: { key: "Digit7" },
|
||||
"&": { key: "Digit7", shift: true },
|
||||
8: { key: "Digit8" },
|
||||
"*": { key: "Digit8", shift: true },
|
||||
9: { key: "Digit9" },
|
||||
"(": { key: "Digit9", shift: true },
|
||||
0: { key: "Digit0" },
|
||||
")": { key: "Digit0", shift: true },
|
||||
"-": { key: "Minus" },
|
||||
_: { key: "Minus", shift: true },
|
||||
"=": { key: "Equal" },
|
||||
"+": { key: "Equal", shift: true },
|
||||
"'": { key: "Quote" },
|
||||
'@': { key: "Quote", shift: true },
|
||||
",": { key: "Comma" },
|
||||
"<": { key: "Comma", shift: true },
|
||||
"/": { key: "Slash" },
|
||||
"?": { key: "Slash", shift: true },
|
||||
".": { key: "Period" },
|
||||
">": { key: "Period", shift: true },
|
||||
";": { key: "Semicolon" },
|
||||
":": { key: "Semicolon", shift: true },
|
||||
"[": { key: "BracketLeft" },
|
||||
"{": { key: "BracketLeft", shift: true },
|
||||
"]": { key: "BracketRight" },
|
||||
"}": { key: "BracketRight", shift: true },
|
||||
"#": { key: "Backslash" },
|
||||
"~": { key: "Backslash", shift: true },
|
||||
"`": { key: "Backquote" },
|
||||
"¬": { key: "Backquote", shift: true },
|
||||
"\\": { key: "IntlBackslash" },
|
||||
"|": { key: "IntlBackslash", shift: true },
|
||||
" ": { key: "Space" },
|
||||
"\n": { key: "Enter" },
|
||||
Enter: { key: "Enter" },
|
||||
Tab: { key: "Tab" },
|
||||
} as Record<string, KeyCombo>
|
|
@ -0,0 +1,113 @@
|
|||
import { KeyCombo } from "../keyboardLayouts"
|
||||
|
||||
export const name = "English (US)";
|
||||
|
||||
export const chars = {
|
||||
A: { key: "KeyA", shift: true },
|
||||
B: { key: "KeyB", shift: true },
|
||||
C: { key: "KeyC", shift: true },
|
||||
D: { key: "KeyD", shift: true },
|
||||
E: { key: "KeyE", shift: true },
|
||||
F: { key: "KeyF", shift: true },
|
||||
G: { key: "KeyG", shift: true },
|
||||
H: { key: "KeyH", shift: true },
|
||||
I: { key: "KeyI", shift: true },
|
||||
J: { key: "KeyJ", shift: true },
|
||||
K: { key: "KeyK", shift: true },
|
||||
L: { key: "KeyL", shift: true },
|
||||
M: { key: "KeyM", shift: true },
|
||||
N: { key: "KeyN", shift: true },
|
||||
O: { key: "KeyO", shift: true },
|
||||
P: { key: "KeyP", shift: true },
|
||||
Q: { key: "KeyQ", shift: true },
|
||||
R: { key: "KeyR", shift: true },
|
||||
S: { key: "KeyS", shift: true },
|
||||
T: { key: "KeyT", shift: true },
|
||||
U: { key: "KeyU", shift: true },
|
||||
V: { key: "KeyV", shift: true },
|
||||
W: { key: "KeyW", shift: true },
|
||||
X: { key: "KeyX", shift: true },
|
||||
Y: { key: "KeyY", shift: true },
|
||||
Z: { key: "KeyZ", shift: true },
|
||||
a: { key: "KeyA" },
|
||||
b: { key: "KeyB" },
|
||||
c: { key: "KeyC" },
|
||||
d: { key: "KeyD" },
|
||||
e: { key: "KeyE" },
|
||||
f: { key: "KeyF" },
|
||||
g: { key: "KeyG" },
|
||||
h: { key: "KeyH" },
|
||||
i: { key: "KeyI" },
|
||||
j: { key: "KeyJ" },
|
||||
k: { key: "KeyK" },
|
||||
l: { key: "KeyL" },
|
||||
m: { key: "KeyM" },
|
||||
n: { key: "KeyN" },
|
||||
o: { key: "KeyO" },
|
||||
p: { key: "KeyP" },
|
||||
q: { key: "KeyQ" },
|
||||
r: { key: "KeyR" },
|
||||
s: { key: "KeyS" },
|
||||
t: { key: "KeyT" },
|
||||
u: { key: "KeyU" },
|
||||
v: { key: "KeyV" },
|
||||
w: { key: "KeyW" },
|
||||
x: { key: "KeyX" },
|
||||
y: { key: "KeyY" },
|
||||
z: { key: "KeyZ" },
|
||||
1: { key: "Digit1" },
|
||||
"!": { key: "Digit1", shift: true },
|
||||
2: { key: "Digit2" },
|
||||
"@": { key: "Digit2", shift: true },
|
||||
3: { key: "Digit3" },
|
||||
"#": { key: "Digit3", shift: true },
|
||||
4: { key: "Digit4" },
|
||||
$: { key: "Digit4", shift: true },
|
||||
"%": { key: "Digit5", shift: true },
|
||||
5: { key: "Digit5" },
|
||||
"^": { key: "Digit6", shift: true },
|
||||
6: { key: "Digit6" },
|
||||
"&": { key: "Digit7", shift: true },
|
||||
7: { key: "Digit7" },
|
||||
"*": { key: "Digit8", shift: true },
|
||||
8: { key: "Digit8" },
|
||||
"(": { key: "Digit9", shift: true },
|
||||
9: { key: "Digit9" },
|
||||
")": { key: "Digit0", shift: true },
|
||||
0: { key: "Digit0" },
|
||||
"-": { key: "Minus" },
|
||||
_: { key: "Minus", shift: true },
|
||||
"=": { key: "Equal" },
|
||||
"+": { key: "Equal", shift: true },
|
||||
"'": { key: "Quote" },
|
||||
'"': { key: "Quote", shift: true },
|
||||
",": { key: "Comma" },
|
||||
"<": { key: "Comma", shift: true },
|
||||
"/": { key: "Slash" },
|
||||
"?": { key: "Slash", shift: true },
|
||||
".": { key: "Period" },
|
||||
">": { key: "Period", shift: true },
|
||||
";": { key: "Semicolon" },
|
||||
":": { key: "Semicolon", shift: true },
|
||||
"[": { key: "BracketLeft" },
|
||||
"{": { key: "BracketLeft", shift: true },
|
||||
"]": { key: "BracketRight" },
|
||||
"}": { key: "BracketRight", shift: true },
|
||||
"\\": { key: "Backslash" },
|
||||
"|": { key: "Backslash", shift: true },
|
||||
"`": { key: "Backquote" },
|
||||
"~": { key: "Backquote", shift: true },
|
||||
"§": { key: "IntlBackslash" },
|
||||
"±": { key: "IntlBackslash", shift: true },
|
||||
" ": { key: "Space", shift: false },
|
||||
"\n": { key: "Enter", shift: false },
|
||||
Enter: { key: "Enter", shift: false },
|
||||
Tab: { key: "Tab", shift: false },
|
||||
PrintScreen: { key: "Prt Sc", shift: false },
|
||||
SystemRequest: { key: "Prt Sc", shift: true },
|
||||
ScrollLock: { key: "ScrollLock", shift: false},
|
||||
Pause: { key: "Pause", shift: false },
|
||||
Break: { key: "Pause", shift: true },
|
||||
Insert: { key: "Insert", shift: false },
|
||||
Delete: { key: "Delete", shift: false },
|
||||
} as Record<string, KeyCombo>
|
|
@ -0,0 +1,168 @@
|
|||
import { KeyCombo } from "../keyboardLayouts"
|
||||
|
||||
export const name = "Español";
|
||||
|
||||
const keyTrema = { key: "Quote", shift: true } // tréma (umlaut), two dots placed above a vowel
|
||||
const keyAcute = { key: "Quote" } // accent aigu (acute accent), mark ´ placed above the letter
|
||||
const keyHat = { key: "BracketRight", shift: true } // accent circonflexe (accent hat), mark ^ placed above the letter
|
||||
const keyGrave = { key: "BracketRight" } // accent grave, mark ` placed above the letter
|
||||
const keyTilde = { key: "Key4", altRight: true } // tilde, mark ~ placed above the letter
|
||||
|
||||
export const chars = {
|
||||
A: { key: "KeyA", shift: true },
|
||||
"Ä": { key: "KeyA", shift: true, accentKey: keyTrema },
|
||||
"Á": { key: "KeyA", shift: true, accentKey: keyAcute },
|
||||
"Â": { key: "KeyA", shift: true, accentKey: keyHat },
|
||||
"À": { key: "KeyA", shift: true, accentKey: keyGrave },
|
||||
"Ã": { key: "KeyA", shift: true, accentKey: keyTilde },
|
||||
B: { key: "KeyB", shift: true },
|
||||
C: { key: "KeyC", shift: true },
|
||||
D: { key: "KeyD", shift: true },
|
||||
E: { key: "KeyE", shift: true },
|
||||
"Ë": { key: "KeyE", shift: true, accentKey: keyTrema },
|
||||
"É": { key: "KeyE", shift: true, accentKey: keyAcute },
|
||||
"Ê": { key: "KeyE", shift: true, accentKey: keyHat },
|
||||
"È": { key: "KeyE", shift: true, accentKey: keyGrave },
|
||||
"Ẽ": { key: "KeyE", shift: true, accentKey: keyTilde },
|
||||
F: { key: "KeyF", shift: true },
|
||||
G: { key: "KeyG", shift: true },
|
||||
H: { key: "KeyH", shift: true },
|
||||
I: { key: "KeyI", shift: true },
|
||||
"Ï": { key: "KeyI", shift: true, accentKey: keyTrema },
|
||||
"Í": { key: "KeyI", shift: true, accentKey: keyAcute },
|
||||
"Î": { key: "KeyI", shift: true, accentKey: keyHat },
|
||||
"Ì": { key: "KeyI", shift: true, accentKey: keyGrave },
|
||||
"Ĩ": { key: "KeyI", shift: true, accentKey: keyTilde },
|
||||
J: { key: "KeyJ", shift: true },
|
||||
K: { key: "KeyK", shift: true },
|
||||
L: { key: "KeyL", shift: true },
|
||||
M: { key: "KeyM", shift: true },
|
||||
N: { key: "KeyN", shift: true },
|
||||
O: { key: "KeyO", shift: true },
|
||||
"Ö": { key: "KeyO", shift: true, accentKey: keyTrema },
|
||||
"Ó": { key: "KeyO", shift: true, accentKey: keyAcute },
|
||||
"Ô": { key: "KeyO", shift: true, accentKey: keyHat },
|
||||
"Ò": { key: "KeyO", shift: true, accentKey: keyGrave },
|
||||
"Õ": { key: "KeyO", shift: true, accentKey: keyTilde },
|
||||
P: { key: "KeyP", shift: true },
|
||||
Q: { key: "KeyQ", shift: true },
|
||||
R: { key: "KeyR", shift: true },
|
||||
S: { key: "KeyS", shift: true },
|
||||
T: { key: "KeyT", shift: true },
|
||||
U: { key: "KeyU", shift: true },
|
||||
"Ü": { key: "KeyU", shift: true, accentKey: keyTrema },
|
||||
"Ú": { key: "KeyU", shift: true, accentKey: keyAcute },
|
||||
"Û": { key: "KeyU", shift: true, accentKey: keyHat },
|
||||
"Ù": { key: "KeyU", shift: true, accentKey: keyGrave },
|
||||
"Ũ": { key: "KeyU", shift: true, accentKey: keyTilde },
|
||||
V: { key: "KeyV", shift: true },
|
||||
W: { key: "KeyW", shift: true },
|
||||
X: { key: "KeyX", shift: true },
|
||||
Y: { key: "KeyY", shift: true },
|
||||
Z: { key: "KeyZ", shift: true },
|
||||
a: { key: "KeyA" },
|
||||
"ä": { key: "KeyA", accentKey: keyTrema },
|
||||
"á": { key: "KeyA", accentKey: keyAcute },
|
||||
"â": { key: "KeyA", accentKey: keyHat },
|
||||
"à": { key: "KeyA", accentKey: keyGrave },
|
||||
"ã": { key: "KeyA", accentKey: keyTilde },
|
||||
b: { key: "KeyB" },
|
||||
c: { key: "KeyC" },
|
||||
d: { key: "KeyD" },
|
||||
e: { key: "KeyE" },
|
||||
"ë": { key: "KeyE", accentKey: keyTrema },
|
||||
"é": { key: "KeyE", accentKey: keyAcute },
|
||||
"ê": { key: "KeyE", accentKey: keyHat },
|
||||
"è": { key: "KeyE", accentKey: keyGrave },
|
||||
"ẽ": { key: "KeyE", accentKey: keyTilde },
|
||||
"€": { key: "KeyE", altRight: true },
|
||||
f: { key: "KeyF" },
|
||||
g: { key: "KeyG" },
|
||||
h: { key: "KeyH" },
|
||||
i: { key: "KeyI" },
|
||||
"ï": { key: "KeyI", accentKey: keyTrema },
|
||||
"í": { key: "KeyI", accentKey: keyAcute },
|
||||
"î": { key: "KeyI", accentKey: keyHat },
|
||||
"ì": { key: "KeyI", accentKey: keyGrave },
|
||||
"ĩ": { key: "KeyI", accentKey: keyTilde },
|
||||
j: { key: "KeyJ" },
|
||||
k: { key: "KeyK" },
|
||||
l: { key: "KeyL" },
|
||||
m: { key: "KeyM" },
|
||||
n: { key: "KeyN" },
|
||||
o: { key: "KeyO" },
|
||||
"ö": { key: "KeyO", accentKey: keyTrema },
|
||||
"ó": { key: "KeyO", accentKey: keyAcute },
|
||||
"ô": { key: "KeyO", accentKey: keyHat },
|
||||
"ò": { key: "KeyO", accentKey: keyGrave },
|
||||
"õ": { key: "KeyO", accentKey: keyTilde },
|
||||
p: { key: "KeyP" },
|
||||
q: { key: "KeyQ" },
|
||||
r: { key: "KeyR" },
|
||||
s: { key: "KeyS" },
|
||||
t: { key: "KeyT" },
|
||||
u: { key: "KeyU" },
|
||||
"ü": { key: "KeyU", accentKey: keyTrema },
|
||||
"ú": { key: "KeyU", accentKey: keyAcute },
|
||||
"û": { key: "KeyU", accentKey: keyHat },
|
||||
"ù": { key: "KeyU", accentKey: keyGrave },
|
||||
"ũ": { key: "KeyU", accentKey: keyTilde },
|
||||
v: { key: "KeyV" },
|
||||
w: { key: "KeyW" },
|
||||
x: { key: "KeyX" },
|
||||
y: { key: "KeyY" },
|
||||
z: { key: "KeyZ" },
|
||||
"º": { key: "Backquote" },
|
||||
"ª": { key: "Backquote", shift: true },
|
||||
"\\": { key: "Backquote", altRight: true },
|
||||
1: { key: "Digit1" },
|
||||
"!": { key: "Digit1", shift: true },
|
||||
"|": { key: "Digit1", altRight: true },
|
||||
2: { key: "Digit2" },
|
||||
"\"": { key: "Digit2", shift: true },
|
||||
"@": { key: "Digit2", altRight: true },
|
||||
3: { key: "Digit3" },
|
||||
"·": { key: "Digit3", shift: true },
|
||||
"#": { key: "Digit3", altRight: true },
|
||||
4: { key: "Digit4" },
|
||||
"$": { key: "Digit4", shift: true },
|
||||
5: { key: "Digit5" },
|
||||
"%": { key: "Digit5", shift: true },
|
||||
6: { key: "Digit6" },
|
||||
"&": { key: "Digit6", shift: true },
|
||||
"¬": { key: "Digit6", altRight: true },
|
||||
7: { key: "Digit7" },
|
||||
"/": { key: "Digit7", shift: true },
|
||||
8: { key: "Digit8" },
|
||||
"(": { key: "Digit8", shift: true },
|
||||
9: { key: "Digit9" },
|
||||
")": { key: "Digit9", shift: true },
|
||||
0: { key: "Digit0" },
|
||||
"=": { key: "Digit0", shift: true },
|
||||
"'": { key: "Minus" },
|
||||
"?": { key: "Minus", shift: true },
|
||||
"¡": { key: "Equal", deadKey: true },
|
||||
"¿": { key: "Equal", shift: true },
|
||||
"[": { key: "BracketLeft", altRight: true },
|
||||
"+": { key: "BracketRight" },
|
||||
"*": { key: "BracketRight", shift: true },
|
||||
"]": { key: "BracketRight", altRight: true },
|
||||
"ñ": { key: "Semicolon" },
|
||||
"Ñ": { key: "Semicolon", shift: true },
|
||||
"{": { key: "Quote", altRight: true },
|
||||
"ç": { key: "Backslash" },
|
||||
"Ç": { key: "Backslash", shift: true },
|
||||
"}": { key: "Backslash", altRight: true },
|
||||
",": { key: "Comma" },
|
||||
";": { key: "Comma", shift: true },
|
||||
".": { key: "Period" },
|
||||
":": { key: "Period", shift: true },
|
||||
"-": { key: "Slash" },
|
||||
"_": { key: "Slash", shift: true },
|
||||
"<": { key: "IntlBackslash" },
|
||||
">": { key: "IntlBackslash", shift: true },
|
||||
" ": { key: "Space" },
|
||||
"\n": { key: "Enter" },
|
||||
Enter: { key: "Enter" },
|
||||
Tab: { key: "Tab" },
|
||||
} as Record<string, KeyCombo>;
|
|
@ -0,0 +1,167 @@
|
|||
import { KeyCombo } from "../keyboardLayouts"
|
||||
|
||||
export const name = "Belgisch Nederlands";
|
||||
|
||||
const keyTrema = { key: "BracketLeft", shift: true } // tréma (umlaut), two dots placed above a vowel
|
||||
const keyHat = { key: "BracketLeft" } // accent circonflexe (accent hat), mark ^ placed above the letter
|
||||
const keyAcute = { key: "Semicolon", altRight: true } // accent aigu (acute accent), mark ´ placed above the letter
|
||||
const keyGrave = { key: "Quote", shift: true } // accent grave, mark ` placed above the letter
|
||||
const keyTilde = { key: "Slash", altRight: true } // tilde, mark ~ placed above the letter
|
||||
|
||||
export const chars = {
|
||||
A: { key: "KeyQ", shift: true },
|
||||
"Ä": { key: "KeyQ", shift: true, accentKey: keyTrema },
|
||||
"Â": { key: "KeyQ", shift: true, accentKey: keyHat },
|
||||
"Á": { key: "KeyQ", shift: true, accentKey: keyAcute },
|
||||
"À": { key: "KeyQ", shift: true, accentKey: keyGrave },
|
||||
"Ã": { key: "KeyQ", shift: true, accentKey: keyTilde },
|
||||
B: { key: "KeyB", shift: true },
|
||||
C: { key: "KeyC", shift: true },
|
||||
D: { key: "KeyD", shift: true },
|
||||
E: { key: "KeyE", shift: true },
|
||||
"Ë": { key: "KeyE", shift: true, accentKey: keyTrema },
|
||||
"Ê": { key: "KeyE", shift: true, accentKey: keyHat },
|
||||
"É": { key: "KeyE", shift: true, accentKey: keyAcute },
|
||||
"È": { key: "KeyE", shift: true, accentKey: keyGrave },
|
||||
"Ẽ": { key: "KeyE", shift: true, accentKey: keyTilde },
|
||||
F: { key: "KeyF", shift: true },
|
||||
G: { key: "KeyG", shift: true },
|
||||
H: { key: "KeyH", shift: true },
|
||||
I: { key: "KeyI", shift: true },
|
||||
"Ï": { key: "KeyI", shift: true, accentKey: keyTrema },
|
||||
"Î": { key: "KeyI", shift: true, accentKey: keyHat },
|
||||
"Í": { key: "KeyI", shift: true, accentKey: keyAcute },
|
||||
"Ì": { key: "KeyI", shift: true, accentKey: keyGrave },
|
||||
"Ĩ": { key: "KeyI", shift: true, accentKey: keyTilde },
|
||||
J: { key: "KeyJ", shift: true },
|
||||
K: { key: "KeyK", shift: true },
|
||||
L: { key: "KeyL", shift: true },
|
||||
M: { key: "Semicolon", shift: true },
|
||||
N: { key: "KeyN", shift: true },
|
||||
O: { key: "KeyO", shift: true },
|
||||
"Ö": { key: "KeyO", shift: true, accentKey: keyTrema },
|
||||
"Ô": { key: "KeyO", shift: true, accentKey: keyHat },
|
||||
"Ó": { key: "KeyO", shift: true, accentKey: keyAcute },
|
||||
"Ò": { key: "KeyO", shift: true, accentKey: keyGrave },
|
||||
"Õ": { key: "KeyO", shift: true, accentKey: keyTilde },
|
||||
P: { key: "KeyP", shift: true },
|
||||
Q: { key: "KeyA", shift: true },
|
||||
R: { key: "KeyR", shift: true },
|
||||
S: { key: "KeyS", shift: true },
|
||||
T: { key: "KeyT", shift: true },
|
||||
U: { key: "KeyU", shift: true },
|
||||
"Ü": { key: "KeyU", shift: true, accentKey: keyTrema },
|
||||
"Û": { key: "KeyU", shift: true, accentKey: keyHat },
|
||||
"Ú": { key: "KeyU", shift: true, accentKey: keyAcute },
|
||||
"Ù": { key: "KeyU", shift: true, accentKey: keyGrave },
|
||||
"Ũ": { key: "KeyU", shift: true, accentKey: keyTilde },
|
||||
V: { key: "KeyV", shift: true },
|
||||
W: { key: "KeyW", shift: true },
|
||||
X: { key: "KeyX", shift: true },
|
||||
Y: { key: "KeyZ", shift: true },
|
||||
Z: { key: "KeyY", shift: true },
|
||||
a: { key: "KeyQ" },
|
||||
"ä": { key: "KeyQ", accentKey: keyTrema },
|
||||
"â": { key: "KeyQ", accentKey: keyHat },
|
||||
"á": { key: "KeyQ", accentKey: keyAcute },
|
||||
"ã": { key: "KeyQ", accentKey: keyTilde },
|
||||
b: { key: "KeyB" },
|
||||
c: { key: "KeyC" },
|
||||
d: { key: "KeyD" },
|
||||
e: { key: "KeyE" },
|
||||
"ë": { key: "KeyE", accentKey: keyTrema },
|
||||
"ê": { key: "KeyE", accentKey: keyHat },
|
||||
"ẽ": { key: "KeyE", accentKey: keyTilde },
|
||||
"€": { key: "KeyE", altRight: true },
|
||||
f: { key: "KeyF" },
|
||||
g: { key: "KeyG" },
|
||||
h: { key: "KeyH" },
|
||||
i: { key: "KeyI" },
|
||||
"ï": { key: "KeyI", accentKey: keyTrema },
|
||||
"î": { key: "KeyI", accentKey: keyHat },
|
||||
"í": { key: "KeyI", accentKey: keyAcute },
|
||||
"ì": { key: "KeyI", accentKey: keyGrave },
|
||||
"ĩ": { key: "KeyI", accentKey: keyTilde },
|
||||
j: { key: "KeyJ" },
|
||||
k: { key: "KeyK" },
|
||||
l: { key: "KeyL" },
|
||||
m: { key: "Semicolon" },
|
||||
n: { key: "KeyN" },
|
||||
o: { key: "KeyO" },
|
||||
"ö": { key: "KeyO", accentKey: keyTrema },
|
||||
"ó": { key: "KeyO", accentKey: keyAcute },
|
||||
"ô": { key: "KeyO", accentKey: keyHat },
|
||||
"ò": { key: "KeyO", accentKey: keyGrave },
|
||||
"õ": { key: "KeyO", accentKey: keyTilde },
|
||||
p: { key: "KeyP" },
|
||||
q: { key: "KeyA" },
|
||||
r: { key: "KeyR" },
|
||||
s: { key: "KeyS" },
|
||||
t: { key: "KeyT" },
|
||||
u: { key: "KeyU" },
|
||||
"ü": { key: "KeyU", accentKey: keyTrema },
|
||||
"û": { key: "KeyU", accentKey: keyHat },
|
||||
"ú": { key: "KeyU", accentKey: keyAcute },
|
||||
"ũ": { key: "KeyU", accentKey: keyTilde },
|
||||
v: { key: "KeyV" },
|
||||
w: { key: "KeyW" },
|
||||
x: { key: "KeyX" },
|
||||
y: { key: "KeyZ" },
|
||||
z: { key: "KeyY" },
|
||||
"²": { key: "Backquote" },
|
||||
"³": { key: "Backquote", shift: true },
|
||||
"&": { key: "Digit1" },
|
||||
1: { key: "Digit1", shift: true },
|
||||
"|": { key: "Digit1", altRight: true },
|
||||
"é": { key: "Digit2" },
|
||||
2: { key: "Digit2", shift: true },
|
||||
"@": { key: "Digit2", altRight: true },
|
||||
"\"": { key: "Digit3" },
|
||||
3: { key: "Digit3", shift: true },
|
||||
"#": { key: "Digit3", altRight: true },
|
||||
"'": { key: "Digit4" },
|
||||
4: { key: "Digit4", shift: true },
|
||||
"(": { key: "Digit5" },
|
||||
5: { key: "Digit5", shift: true },
|
||||
"§": { key: "Digit6" },
|
||||
6: { key: "Digit6", shift: true },
|
||||
"^": { key: "Digit6", altRight: true },
|
||||
"è": { key: "Digit7" },
|
||||
7: { key: "Digit7", shift: true },
|
||||
"!": { key: "Digit8" },
|
||||
8: { key: "Digit8", shift: true },
|
||||
"ç": { key: "Digit9" },
|
||||
9: { key: "Digit9", shift: true },
|
||||
"{": { key: "Digit9", altRight: true },
|
||||
"à": { key: "Digit0" },
|
||||
0: { key: "Digit0", shift: true },
|
||||
"}": { key: "Digit0", altRight: true },
|
||||
")": { key: "Minus" },
|
||||
"°": { key: "Minus", shift: true },
|
||||
"-": { key: "Equal", deadKey: true },
|
||||
"_": { key: "Equal", shift: true },
|
||||
"[": { key: "BracketLeft", altRight: true },
|
||||
"$": { key: "BracketRight" },
|
||||
"*": { key: "BracketRight", altRight: true },
|
||||
"]": { key: "BracketRight", altRight: true },
|
||||
"ù": { key: "Quote" },
|
||||
"%": { key: "Quote", shift: true },
|
||||
"µ": { key: "Backslash" },
|
||||
"£": { key: "Backslash", shift: true },
|
||||
",": { key: "KeyM" },
|
||||
"?": { key: "KeyM", shift: true },
|
||||
";": { key: "Comma" },
|
||||
".": { key: "Comma", shift: true },
|
||||
":": { key: "Period" },
|
||||
"/": { key: "Period", shift: true },
|
||||
"=": { key: "Slash" },
|
||||
"+": { key: "Slash", shift: true },
|
||||
"~": { key: "Slash", deadKey: true },
|
||||
"<": { key: "IntlBackslash" },
|
||||
">": { key: "IntlBackslash", shift: true },
|
||||
"\\": { key: "IntlBackslash", altRight: true },
|
||||
" ": { key: "Space" },
|
||||
"\n": { key: "Enter" },
|
||||
Enter: { key: "Enter" },
|
||||
Tab: { key: "Tab" },
|
||||
} as Record<string, KeyCombo>;
|
|
@ -0,0 +1,14 @@
|
|||
import { KeyCombo } from "../keyboardLayouts"
|
||||
import { chars as chars_de_CH } from "./de_CH"
|
||||
|
||||
export const name = "Français de Suisse";
|
||||
|
||||
export const chars = {
|
||||
...chars_de_CH,
|
||||
"è": { key: "BracketLeft" },
|
||||
"ü": { key: "BracketLeft", shift: true },
|
||||
"é": { key: "Semicolon" },
|
||||
"ö": { key: "Semicolon", shift: true },
|
||||
"à": { key: "Quote" },
|
||||
"ä": { key: "Quote", shift: true },
|
||||
} as Record<string, KeyCombo>;
|
|
@ -0,0 +1,139 @@
|
|||
import { KeyCombo } from "../keyboardLayouts"
|
||||
|
||||
export const name = "Français";
|
||||
|
||||
const keyTrema = { key: "BracketLeft", shift: true } // tréma (umlaut), two dots placed above a vowel
|
||||
const keyHat = { key: "BracketLeft" } // accent circonflexe (accent hat), mark ^ placed above the letter
|
||||
|
||||
export const chars = {
|
||||
A: { key: "KeyQ", shift: true },
|
||||
"Ä": { key: "KeyQ", shift: true, accentKey: keyTrema },
|
||||
"Â": { key: "KeyQ", shift: true, accentKey: keyHat },
|
||||
B: { key: "KeyB", shift: true },
|
||||
C: { key: "KeyC", shift: true },
|
||||
D: { key: "KeyD", shift: true },
|
||||
E: { key: "KeyE", shift: true },
|
||||
"Ë": { key: "KeyE", shift: true, accentKey: keyTrema },
|
||||
"Ê": { key: "KeyE", shift: true, accentKey: keyHat },
|
||||
F: { key: "KeyF", shift: true },
|
||||
G: { key: "KeyG", shift: true },
|
||||
H: { key: "KeyH", shift: true },
|
||||
I: { key: "KeyI", shift: true },
|
||||
"Ï": { key: "KeyI", shift: true, accentKey: keyTrema },
|
||||
"Î": { key: "KeyI", shift: true, accentKey: keyHat },
|
||||
J: { key: "KeyJ", shift: true },
|
||||
K: { key: "KeyK", shift: true },
|
||||
L: { key: "KeyL", shift: true },
|
||||
M: { key: "Semicolon", shift: true },
|
||||
N: { key: "KeyN", shift: true },
|
||||
O: { key: "KeyO", shift: true },
|
||||
"Ö": { key: "KeyO", shift: true, accentKey: keyTrema },
|
||||
"Ô": { key: "KeyO", shift: true, accentKey: keyHat },
|
||||
P: { key: "KeyP", shift: true },
|
||||
Q: { key: "KeyA", shift: true },
|
||||
R: { key: "KeyR", shift: true },
|
||||
S: { key: "KeyS", shift: true },
|
||||
T: { key: "KeyT", shift: true },
|
||||
U: { key: "KeyU", shift: true },
|
||||
"Ü": { key: "KeyU", shift: true, accentKey: keyTrema },
|
||||
"Û": { key: "KeyU", shift: true, accentKey: keyHat },
|
||||
V: { key: "KeyV", shift: true },
|
||||
W: { key: "KeyZ", shift: true },
|
||||
X: { key: "KeyX", shift: true },
|
||||
Y: { key: "KeyY", shift: true },
|
||||
Z: { key: "KeyW", shift: true },
|
||||
a: { key: "KeyQ" },
|
||||
"ä": { key: "KeyQ", accentKey: keyTrema },
|
||||
"â": { key: "KeyQ", accentKey: keyHat },
|
||||
b: { key: "KeyB" },
|
||||
c: { key: "KeyC" },
|
||||
d: { key: "KeyD" },
|
||||
e: { key: "KeyE" },
|
||||
"ë": { key: "KeyE", accentKey: keyTrema },
|
||||
"ê": { key: "KeyE", accentKey: keyHat },
|
||||
"€": { key: "KeyE", altRight: true },
|
||||
f: { key: "KeyF" },
|
||||
g: { key: "KeyG" },
|
||||
h: { key: "KeyH" },
|
||||
i: { key: "KeyI" },
|
||||
"ï": { key: "KeyI", accentKey: keyTrema },
|
||||
"î": { key: "KeyI", accentKey: keyHat },
|
||||
j: { key: "KeyJ" },
|
||||
k: { key: "KeyK" },
|
||||
l: { key: "KeyL" },
|
||||
m: { key: "Semicolon" },
|
||||
n: { key: "KeyN" },
|
||||
o: { key: "KeyO" },
|
||||
"ö": { key: "KeyO", accentKey: keyTrema },
|
||||
"ô": { key: "KeyO", accentKey: keyHat },
|
||||
p: { key: "KeyP" },
|
||||
q: { key: "KeyA" },
|
||||
r: { key: "KeyR" },
|
||||
s: { key: "KeyS" },
|
||||
t: { key: "KeyT" },
|
||||
u: { key: "KeyU" },
|
||||
"ü": { key: "KeyU", accentKey: keyTrema },
|
||||
"û": { key: "KeyU", accentKey: keyHat },
|
||||
v: { key: "KeyV" },
|
||||
w: { key: "KeyZ" },
|
||||
x: { key: "KeyX" },
|
||||
y: { key: "KeyY" },
|
||||
z: { key: "KeyW" },
|
||||
"²": { key: "Backquote" },
|
||||
"&": { key: "Digit1" },
|
||||
1: { key: "Digit1", shift: true },
|
||||
"é": { key: "Digit2" },
|
||||
2: { key: "Digit2", shift: true },
|
||||
"~": { key: "Digit2", altRight: true },
|
||||
"\"": { key: "Digit3" },
|
||||
3: { key: "Digit3", shift: true },
|
||||
"#": { key: "Digit3", altRight: true },
|
||||
"'": { key: "Digit4" },
|
||||
4: { key: "Digit4", shift: true },
|
||||
"{": { key: "Digit4", altRight: true },
|
||||
"(": { key: "Digit5" },
|
||||
5: { key: "Digit5", shift: true },
|
||||
"[": { key: "Digit5", altRight: true },
|
||||
"-": { key: "Digit6" },
|
||||
6: { key: "Digit6", shift: true },
|
||||
"|": { key: "Digit6", altRight: true },
|
||||
"è": { key: "Digit7" },
|
||||
7: { key: "Digit7", shift: true },
|
||||
"`": { key: "Digit7", altRight: true },
|
||||
"_": { key: "Digit8" },
|
||||
8: { key: "Digit8", shift: true },
|
||||
"\\": { key: "Digit8", altRight: true },
|
||||
"ç": { key: "Digit9" },
|
||||
9: { key: "Digit9", shift: true },
|
||||
"^": { key: "Digit9", altRight: true },
|
||||
"à": { key: "Digit0" },
|
||||
0: { key: "Digit0", shift: true },
|
||||
"@": { key: "Digit0", altRight: true },
|
||||
")": { key: "Minus" },
|
||||
"°": { key: "Minus", shift: true },
|
||||
"]": { key: "Minus", altRight: true },
|
||||
"=": { key: "Equal" },
|
||||
"+": { key: "Equal", shift: true },
|
||||
"}": { key: "Equal", altRight: true },
|
||||
"$": { key: "BracketRight" },
|
||||
"£": { key: "BracketRight", shift: true },
|
||||
"¤": { key: "BracketRight", altRight: true },
|
||||
"ù": { key: "Quote" },
|
||||
"%": { key: "Quote", shift: true },
|
||||
"*": { key: "Backslash" },
|
||||
"µ": { key: "Backslash", shift: true },
|
||||
",": { key: "KeyM" },
|
||||
"?": { key: "KeyM", shift: true },
|
||||
";": { key: "Comma" },
|
||||
".": { key: "Comma", shift: true },
|
||||
":": { key: "Period" },
|
||||
"/": { key: "Period", shift: true },
|
||||
"!": { key: "Slash" },
|
||||
"§": { key: "Slash", shift: true },
|
||||
"<": { key: "IntlBackslash" },
|
||||
">": { key: "IntlBackslash", shift: true },
|
||||
" ": { key: "Space" },
|
||||
"\n": { key: "Enter" },
|
||||
Enter: { key: "Enter" },
|
||||
Tab: { key: "Tab" },
|
||||
} as Record<string, KeyCombo>;
|
|
@ -0,0 +1,113 @@
|
|||
import { KeyCombo } from "../keyboardLayouts"
|
||||
|
||||
export const name = "Italiano";
|
||||
|
||||
export const chars = {
|
||||
A: { key: "KeyA", shift: true },
|
||||
B: { key: "KeyB", shift: true },
|
||||
C: { key: "KeyC", shift: true },
|
||||
D: { key: "KeyD", shift: true },
|
||||
E: { key: "KeyE", shift: true },
|
||||
F: { key: "KeyF", shift: true },
|
||||
G: { key: "KeyG", shift: true },
|
||||
H: { key: "KeyH", shift: true },
|
||||
I: { key: "KeyI", shift: true },
|
||||
J: { key: "KeyJ", shift: true },
|
||||
K: { key: "KeyK", shift: true },
|
||||
L: { key: "KeyL", shift: true },
|
||||
M: { key: "KeyM", shift: true },
|
||||
N: { key: "KeyN", shift: true },
|
||||
O: { key: "KeyO", shift: true },
|
||||
P: { key: "KeyP", shift: true },
|
||||
Q: { key: "KeyQ", shift: true },
|
||||
R: { key: "KeyR", shift: true },
|
||||
S: { key: "KeyS", shift: true },
|
||||
T: { key: "KeyT", shift: true },
|
||||
U: { key: "KeyU", shift: true },
|
||||
V: { key: "KeyV", shift: true },
|
||||
W: { key: "KeyW", shift: true },
|
||||
X: { key: "KeyX", shift: true },
|
||||
Y: { key: "KeyY", shift: true },
|
||||
Z: { key: "KeyZ", shift: true },
|
||||
a: { key: "KeyA" },
|
||||
b: { key: "KeyB" },
|
||||
c: { key: "KeyC" },
|
||||
d: { key: "KeyD" },
|
||||
e: { key: "KeyE" },
|
||||
"€": { key: "KeyE", altRight: true },
|
||||
f: { key: "KeyF" },
|
||||
g: { key: "KeyG" },
|
||||
h: { key: "KeyH" },
|
||||
i: { key: "KeyI" },
|
||||
j: { key: "KeyJ" },
|
||||
k: { key: "KeyK" },
|
||||
l: { key: "KeyL" },
|
||||
m: { key: "KeyM" },
|
||||
n: { key: "KeyN" },
|
||||
o: { key: "KeyO" },
|
||||
p: { key: "KeyP" },
|
||||
q: { key: "KeyQ" },
|
||||
r: { key: "KeyR" },
|
||||
s: { key: "KeyS" },
|
||||
t: { key: "KeyT" },
|
||||
u: { key: "KeyU" },
|
||||
v: { key: "KeyV" },
|
||||
w: { key: "KeyW" },
|
||||
x: { key: "KeyX" },
|
||||
y: { key: "KeyY" },
|
||||
z: { key: "KeyZ" },
|
||||
"\\": { key: "Backquote" },
|
||||
"|": { key: "Backquote", shift: true },
|
||||
1: { key: "Digit1" },
|
||||
"!": { key: "Digit1", shift: true },
|
||||
2: { key: "Digit2" },
|
||||
"\"": { key: "Digit2", shift: true },
|
||||
3: { key: "Digit3" },
|
||||
"£": { key: "Digit3", shift: true },
|
||||
4: { key: "Digit4" },
|
||||
"$": { key: "Digit4", shift: true },
|
||||
5: { key: "Digit5" },
|
||||
"%": { key: "Digit5", shift: true },
|
||||
6: { key: "Digit6" },
|
||||
"&": { key: "Digit6", shift: true },
|
||||
7: { key: "Digit7" },
|
||||
"/": { key: "Digit7", shift: true },
|
||||
8: { key: "Digit8" },
|
||||
"(": { key: "Digit8", shift: true },
|
||||
9: { key: "Digit9" },
|
||||
")": { key: "Digit9", shift: true },
|
||||
0: { key: "Digit0" },
|
||||
"=": { key: "Digit0", shift: true },
|
||||
"'": { key: "Minus" },
|
||||
"?": { key: "Minus", shift: true },
|
||||
"ì": { key: "Equal" },
|
||||
"^": { key: "Equal", shift: true },
|
||||
"è": { key: "BracketLeft" },
|
||||
"é": { key: "BracketLeft", shift: true },
|
||||
"[": { key: "BracketLeft", altRight: true },
|
||||
"{": { key: "BracketLeft", shift: true, altRight: true },
|
||||
"+": { key: "BracketRight" },
|
||||
"*": { key: "BracketRight", shift: true },
|
||||
"]": { key: "BracketRight", altRight: true },
|
||||
"}": { key: "BracketRight", shift: true, altRight: true },
|
||||
"ò": { key: "Semicolon" },
|
||||
"ç": { key: "Semicolon", shift: true },
|
||||
"@": { key: "Semicolon", altRight: true },
|
||||
"à": { key: "Quote" },
|
||||
"°": { key: "Quote", shift: true },
|
||||
"#": { key: "Quote", altRight: true },
|
||||
"ù": { key: "Backslash" },
|
||||
"§": { key: "Backslash", shift: true },
|
||||
",": { key: "Comma" },
|
||||
";": { key: "Comma", shift: true },
|
||||
".": { key: "Period" },
|
||||
":": { key: "Period", shift: true },
|
||||
"-": { key: "Slash" },
|
||||
"_": { key: "Slash", shift: true },
|
||||
"<": { key: "IntlBackslash" },
|
||||
">": { key: "IntlBackslash", shift: true },
|
||||
" ": { key: "Space" },
|
||||
"\n": { key: "Enter" },
|
||||
Enter: { key: "Enter" },
|
||||
Tab: { key: "Tab" },
|
||||
} as Record<string, KeyCombo>;
|
|
@ -0,0 +1,167 @@
|
|||
import { KeyCombo } from "../keyboardLayouts"
|
||||
|
||||
export const name = "Norsk bokmål";
|
||||
|
||||
const keyTrema = { key: "BracketRight" } // tréma (umlaut), two dots placed above a vowel
|
||||
const keyAcute = { key: "Equal", altRight: true } // accent aigu (acute accent), mark ´ placed above the letter
|
||||
const keyHat = { key: "BracketRight", shift: true } // accent circonflexe (accent hat), mark ^ placed above the letter
|
||||
const keyGrave = { key: "Equal", shift: true } // accent grave, mark ` placed above the letter
|
||||
const keyTilde = { key: "BracketRight", altRight: true } // tilde, mark ~ placed above the letter
|
||||
|
||||
export const chars = {
|
||||
A: { key: "KeyA", shift: true },
|
||||
"Ä": { key: "KeyA", shift: true, accentKey: keyTrema },
|
||||
"Á": { key: "KeyA", shift: true, accentKey: keyAcute },
|
||||
"Â": { key: "KeyA", shift: true, accentKey: keyHat },
|
||||
"À": { key: "KeyA", shift: true, accentKey: keyGrave },
|
||||
"Ã": { key: "KeyA", shift: true, accentKey: keyTilde },
|
||||
B: { key: "KeyB", shift: true },
|
||||
C: { key: "KeyC", shift: true },
|
||||
D: { key: "KeyD", shift: true },
|
||||
E: { key: "KeyE", shift: true },
|
||||
"Ë": { key: "KeyE", shift: true, accentKey: keyTrema },
|
||||
"É": { key: "KeyE", shift: true, accentKey: keyAcute },
|
||||
"Ê": { key: "KeyE", shift: true, accentKey: keyHat },
|
||||
"È": { key: "KeyE", shift: true, accentKey: keyGrave },
|
||||
"Ẽ": { key: "KeyE", shift: true, accentKey: keyTilde },
|
||||
F: { key: "KeyF", shift: true },
|
||||
G: { key: "KeyG", shift: true },
|
||||
H: { key: "KeyH", shift: true },
|
||||
I: { key: "KeyI", shift: true },
|
||||
"Ï": { key: "KeyI", shift: true, accentKey: keyTrema },
|
||||
"Í": { key: "KeyI", shift: true, accentKey: keyAcute },
|
||||
"Î": { key: "KeyI", shift: true, accentKey: keyHat },
|
||||
"Ì": { key: "KeyI", shift: true, accentKey: keyGrave },
|
||||
"Ĩ": { key: "KeyI", shift: true, accentKey: keyTilde },
|
||||
J: { key: "KeyJ", shift: true },
|
||||
K: { key: "KeyK", shift: true },
|
||||
L: { key: "KeyL", shift: true },
|
||||
M: { key: "KeyM", shift: true },
|
||||
N: { key: "KeyN", shift: true },
|
||||
O: { key: "KeyO", shift: true },
|
||||
"Ö": { key: "KeyO", shift: true, accentKey: keyTrema },
|
||||
"Ó": { key: "KeyO", shift: true, accentKey: keyAcute },
|
||||
"Ô": { key: "KeyO", shift: true, accentKey: keyHat },
|
||||
"Ò": { key: "KeyO", shift: true, accentKey: keyGrave },
|
||||
"Õ": { key: "KeyO", shift: true, accentKey: keyTilde },
|
||||
P: { key: "KeyP", shift: true },
|
||||
Q: { key: "KeyQ", shift: true },
|
||||
R: { key: "KeyR", shift: true },
|
||||
S: { key: "KeyS", shift: true },
|
||||
T: { key: "KeyT", shift: true },
|
||||
U: { key: "KeyU", shift: true },
|
||||
"Ü": { key: "KeyU", shift: true, accentKey: keyTrema },
|
||||
"Ú": { key: "KeyU", shift: true, accentKey: keyAcute },
|
||||
"Û": { key: "KeyU", shift: true, accentKey: keyHat },
|
||||
"Ù": { key: "KeyU", shift: true, accentKey: keyGrave },
|
||||
"Ũ": { key: "KeyU", shift: true, accentKey: keyTilde },
|
||||
V: { key: "KeyV", shift: true },
|
||||
W: { key: "KeyW", shift: true },
|
||||
X: { key: "KeyX", shift: true },
|
||||
Y: { key: "KeyZ", shift: true },
|
||||
Z: { key: "KeyY", shift: true },
|
||||
a: { key: "KeyA" },
|
||||
"ä": { key: "KeyA", accentKey: keyTrema },
|
||||
"á": { key: "KeyA", accentKey: keyAcute },
|
||||
"â": { key: "KeyA", accentKey: keyHat },
|
||||
"à": { key: "KeyA", accentKey: keyGrave },
|
||||
"ã": { key: "KeyA", accentKey: keyTilde },
|
||||
b: { key: "KeyB" },
|
||||
c: { key: "KeyC" },
|
||||
d: { key: "KeyD" },
|
||||
e: { key: "KeyE" },
|
||||
"ë": { key: "KeyE", accentKey: keyTrema },
|
||||
"é": { key: "KeyE", accentKey: keyAcute },
|
||||
"ê": { key: "KeyE", accentKey: keyHat },
|
||||
"è": { key: "KeyE", accentKey: keyGrave },
|
||||
"ẽ": { key: "KeyE", accentKey: keyTilde },
|
||||
"€": { key: "KeyE", altRight: true },
|
||||
f: { key: "KeyF" },
|
||||
g: { key: "KeyG" },
|
||||
h: { key: "KeyH" },
|
||||
i: { key: "KeyI" },
|
||||
"ï": { key: "KeyI", accentKey: keyTrema },
|
||||
"í": { key: "KeyI", accentKey: keyAcute },
|
||||
"î": { key: "KeyI", accentKey: keyHat },
|
||||
"ì": { key: "KeyI", accentKey: keyGrave },
|
||||
"ĩ": { key: "KeyI", accentKey: keyTilde },
|
||||
j: { key: "KeyJ" },
|
||||
k: { key: "KeyK" },
|
||||
l: { key: "KeyL" },
|
||||
m: { key: "KeyM" },
|
||||
n: { key: "KeyN" },
|
||||
o: { key: "KeyO" },
|
||||
"ö": { key: "KeyO", accentKey: keyTrema },
|
||||
"ó": { key: "KeyO", accentKey: keyAcute },
|
||||
"ô": { key: "KeyO", accentKey: keyHat },
|
||||
"ò": { key: "KeyO", accentKey: keyGrave },
|
||||
"õ": { key: "KeyO", accentKey: keyTilde },
|
||||
p: { key: "KeyP" },
|
||||
q: { key: "KeyQ" },
|
||||
r: { key: "KeyR" },
|
||||
s: { key: "KeyS" },
|
||||
t: { key: "KeyT" },
|
||||
u: { key: "KeyU" },
|
||||
"ü": { key: "KeyU", accentKey: keyTrema },
|
||||
"ú": { key: "KeyU", accentKey: keyAcute },
|
||||
"û": { key: "KeyU", accentKey: keyHat },
|
||||
"ù": { key: "KeyU", accentKey: keyGrave },
|
||||
"ũ": { key: "KeyU", accentKey: keyTilde },
|
||||
v: { key: "KeyV" },
|
||||
w: { key: "KeyW" },
|
||||
x: { key: "KeyX" },
|
||||
y: { key: "KeyZ" },
|
||||
z: { key: "KeyY" },
|
||||
"|": { key: "Backquote" },
|
||||
"§": { key: "Backquote", shift: true },
|
||||
1: { key: "Digit1" },
|
||||
"!": { key: "Digit1", shift: true },
|
||||
2: { key: "Digit2" },
|
||||
"\"": { key: "Digit2", shift: true },
|
||||
"@": { key: "Digit2", altRight: true },
|
||||
3: { key: "Digit3" },
|
||||
"#": { key: "Digit3", shift: true },
|
||||
"£": { key: "Digit3", altRight: true },
|
||||
4: { key: "Digit4" },
|
||||
"¤": { key: "Digit4", shift: true },
|
||||
"$": { key: "Digit4", altRight: true },
|
||||
5: { key: "Digit5" },
|
||||
"%": { key: "Digit5", shift: true },
|
||||
6: { key: "Digit6" },
|
||||
"&": { key: "Digit6", shift: true },
|
||||
7: { key: "Digit7" },
|
||||
"/": { key: "Digit7", shift: true },
|
||||
"{": { key: "Digit7", altRight: true },
|
||||
8: { key: "Digit8" },
|
||||
"(": { key: "Digit8", shift: true },
|
||||
"[": { key: "Digit8", altRight: true },
|
||||
9: { key: "Digit9" },
|
||||
")": { key: "Digit9", shift: true },
|
||||
"]": { key: "Digit9", altRight: true },
|
||||
0: { key: "Digit0" },
|
||||
"=": { key: "Digit0", shift: true },
|
||||
"}": { key: "Digit0", altRight: true },
|
||||
"+": { key: "Minus" },
|
||||
"?": { key: "Minus", shift: true },
|
||||
"\\": { key: "Equal" },
|
||||
"å": { key: "BracketLeft" },
|
||||
"Å": { key: "BracketLeft", shift: true },
|
||||
"ø": { key: "Semicolon" },
|
||||
"Ø": { key: "Semicolon", shift: true },
|
||||
"æ": { key: "Quote" },
|
||||
"Æ": { key: "Quote", shift: true },
|
||||
"'": { key: "Backslash" },
|
||||
"*": { key: "Backslash", shift: true },
|
||||
",": { key: "Comma" },
|
||||
";": { key: "Comma", shift: true },
|
||||
".": { key: "Period" },
|
||||
":": { key: "Period", shift: true },
|
||||
"-": { key: "Slash" },
|
||||
"_": { key: "Slash", shift: true },
|
||||
"<": { key: "IntlBackslash" },
|
||||
">": { key: "IntlBackslash", shift: true },
|
||||
" ": { key: "Space" },
|
||||
"\n": { key: "Enter" },
|
||||
Enter: { key: "Enter" },
|
||||
Tab: { key: "Tab" },
|
||||
} as Record<string, KeyCombo>;
|
|
@ -0,0 +1,164 @@
|
|||
import { KeyCombo } from "../keyboardLayouts"
|
||||
|
||||
export const name = "Svenska";
|
||||
|
||||
const keyTrema = { key: "BracketRight" } // tréma (umlaut), two dots placed above a vowel
|
||||
const keyAcute = { key: "Equal" } // accent aigu (acute accent), mark ´ placed above the letter
|
||||
const keyHat = { key: "BracketRight", shift: true } // accent circonflexe (accent hat), mark ^ placed above the letter
|
||||
const keyGrave = { key: "Equal", shift: true } // accent grave, mark ` placed above the letter
|
||||
const keyTilde = { key: "BracketRight", altRight: true } // tilde, mark ~ placed above the letter
|
||||
|
||||
export const chars = {
|
||||
A: { key: "KeyA", shift: true },
|
||||
"Á": { key: "KeyA", shift: true, accentKey: keyAcute },
|
||||
"Â": { key: "KeyA", shift: true, accentKey: keyHat },
|
||||
"À": { key: "KeyA", shift: true, accentKey: keyGrave },
|
||||
"Ã": { key: "KeyA", shift: true, accentKey: keyTilde },
|
||||
B: { key: "KeyB", shift: true },
|
||||
C: { key: "KeyC", shift: true },
|
||||
D: { key: "KeyD", shift: true },
|
||||
E: { key: "KeyE", shift: true },
|
||||
"Ë": { key: "KeyE", shift: true, accentKey: keyTrema },
|
||||
"É": { key: "KeyE", shift: true, accentKey: keyAcute },
|
||||
"Ê": { key: "KeyE", shift: true, accentKey: keyHat },
|
||||
"È": { key: "KeyE", shift: true, accentKey: keyGrave },
|
||||
"Ẽ": { key: "KeyE", shift: true, accentKey: keyTilde },
|
||||
F: { key: "KeyF", shift: true },
|
||||
G: { key: "KeyG", shift: true },
|
||||
H: { key: "KeyH", shift: true },
|
||||
I: { key: "KeyI", shift: true },
|
||||
"Ï": { key: "KeyI", shift: true, accentKey: keyTrema },
|
||||
"Í": { key: "KeyI", shift: true, accentKey: keyAcute },
|
||||
"Î": { key: "KeyI", shift: true, accentKey: keyHat },
|
||||
"Ì": { key: "KeyI", shift: true, accentKey: keyGrave },
|
||||
"Ĩ": { key: "KeyI", shift: true, accentKey: keyTilde },
|
||||
J: { key: "KeyJ", shift: true },
|
||||
K: { key: "KeyK", shift: true },
|
||||
L: { key: "KeyL", shift: true },
|
||||
M: { key: "KeyM", shift: true },
|
||||
N: { key: "KeyN", shift: true },
|
||||
O: { key: "KeyO", shift: true },
|
||||
"Ó": { key: "KeyO", shift: true, accentKey: keyAcute },
|
||||
"Ô": { key: "KeyO", shift: true, accentKey: keyHat },
|
||||
"Ò": { key: "KeyO", shift: true, accentKey: keyGrave },
|
||||
"Õ": { key: "KeyO", shift: true, accentKey: keyTilde },
|
||||
P: { key: "KeyP", shift: true },
|
||||
Q: { key: "KeyQ", shift: true },
|
||||
R: { key: "KeyR", shift: true },
|
||||
S: { key: "KeyS", shift: true },
|
||||
T: { key: "KeyT", shift: true },
|
||||
U: { key: "KeyU", shift: true },
|
||||
"Ü": { key: "KeyU", shift: true, accentKey: keyTrema },
|
||||
"Ú": { key: "KeyU", shift: true, accentKey: keyAcute },
|
||||
"Û": { key: "KeyU", shift: true, accentKey: keyHat },
|
||||
"Ù": { key: "KeyU", shift: true, accentKey: keyGrave },
|
||||
"Ũ": { key: "KeyU", shift: true, accentKey: keyTilde },
|
||||
V: { key: "KeyV", shift: true },
|
||||
W: { key: "KeyW", shift: true },
|
||||
X: { key: "KeyX", shift: true },
|
||||
Y: { key: "KeyZ", shift: true },
|
||||
Z: { key: "KeyY", shift: true },
|
||||
a: { key: "KeyA" },
|
||||
"á": { key: "KeyA", accentKey: keyAcute },
|
||||
"â": { key: "KeyA", accentKey: keyHat },
|
||||
"à": { key: "KeyA", accentKey: keyGrave },
|
||||
"ã": { key: "KeyA", accentKey: keyTilde },
|
||||
b: { key: "KeyB" },
|
||||
c: { key: "KeyC" },
|
||||
d: { key: "KeyD" },
|
||||
e: { key: "KeyE" },
|
||||
"ë": { key: "KeyE", accentKey: keyTrema },
|
||||
"é": { key: "KeyE", accentKey: keyAcute },
|
||||
"ê": { key: "KeyE", accentKey: keyHat },
|
||||
"è": { key: "KeyE", accentKey: keyGrave },
|
||||
"ẽ": { key: "KeyE", accentKey: keyTilde },
|
||||
"€": { key: "KeyE", altRight: true },
|
||||
f: { key: "KeyF" },
|
||||
g: { key: "KeyG" },
|
||||
h: { key: "KeyH" },
|
||||
i: { key: "KeyI" },
|
||||
"ï": { key: "KeyI", accentKey: keyTrema },
|
||||
"í": { key: "KeyI", accentKey: keyAcute },
|
||||
"î": { key: "KeyI", accentKey: keyHat },
|
||||
"ì": { key: "KeyI", accentKey: keyGrave },
|
||||
"ĩ": { key: "KeyI", accentKey: keyTilde },
|
||||
j: { key: "KeyJ" },
|
||||
k: { key: "KeyK" },
|
||||
l: { key: "KeyL" },
|
||||
m: { key: "KeyM" },
|
||||
n: { key: "KeyN" },
|
||||
o: { key: "KeyO" },
|
||||
"ó": { key: "KeyO", accentKey: keyAcute },
|
||||
"ô": { key: "KeyO", accentKey: keyHat },
|
||||
"ò": { key: "KeyO", accentKey: keyGrave },
|
||||
"õ": { key: "KeyO", accentKey: keyTilde },
|
||||
p: { key: "KeyP" },
|
||||
q: { key: "KeyQ" },
|
||||
r: { key: "KeyR" },
|
||||
s: { key: "KeyS" },
|
||||
t: { key: "KeyT" },
|
||||
u: { key: "KeyU" },
|
||||
"ü": { key: "KeyU", accentKey: keyTrema },
|
||||
"ú": { key: "KeyU", accentKey: keyAcute },
|
||||
"û": { key: "KeyU", accentKey: keyHat },
|
||||
"ù": { key: "KeyU", accentKey: keyGrave },
|
||||
"ũ": { key: "KeyU", accentKey: keyTilde },
|
||||
v: { key: "KeyV" },
|
||||
w: { key: "KeyW" },
|
||||
x: { key: "KeyX" },
|
||||
y: { key: "KeyZ" },
|
||||
z: { key: "KeyY" },
|
||||
"§": { key: "Backquote" },
|
||||
"½": { key: "Backquote", shift: true },
|
||||
1: { key: "Digit1" },
|
||||
"!": { key: "Digit1", shift: true },
|
||||
2: { key: "Digit2" },
|
||||
"\"": { key: "Digit2", shift: true },
|
||||
"@": { key: "Digit2", altRight: true },
|
||||
3: { key: "Digit3" },
|
||||
"#": { key: "Digit3", shift: true },
|
||||
"£": { key: "Digit3", altRight: true },
|
||||
4: { key: "Digit4" },
|
||||
"¤": { key: "Digit4", shift: true },
|
||||
"$": { key: "Digit4", altRight: true },
|
||||
5: { key: "Digit5" },
|
||||
"%": { key: "Digit5", shift: true },
|
||||
6: { key: "Digit6" },
|
||||
"&": { key: "Digit6", shift: true },
|
||||
7: { key: "Digit7" },
|
||||
"/": { key: "Digit7", shift: true },
|
||||
"{": { key: "Digit7", altRight: true },
|
||||
8: { key: "Digit8" },
|
||||
"(": { key: "Digit8", shift: true },
|
||||
"[": { key: "Digit8", altRight: true },
|
||||
9: { key: "Digit9" },
|
||||
")": { key: "Digit9", shift: true },
|
||||
"]": { key: "Digit9", altRight: true },
|
||||
0: { key: "Digit0" },
|
||||
"=": { key: "Digit0", shift: true },
|
||||
"}": { key: "Digit0", altRight: true },
|
||||
"+": { key: "Minus" },
|
||||
"?": { key: "Minus", shift: true },
|
||||
"\\": { key: "Minus", altRight: true },
|
||||
"å": { key: "BracketLeft" },
|
||||
"Å": { key: "BracketLeft", shift: true },
|
||||
"ö": { key: "Semicolon" },
|
||||
"Ö": { key: "Semicolon", shift: true },
|
||||
"ä": { key: "Quote" },
|
||||
"Ä": { key: "Quote", shift: true },
|
||||
"'": { key: "Backslash" },
|
||||
"*": { key: "Backslash", shift: true },
|
||||
",": { key: "Comma" },
|
||||
";": { key: "Comma", shift: true },
|
||||
".": { key: "Period" },
|
||||
":": { key: "Period", shift: true },
|
||||
"-": { key: "Slash" },
|
||||
"_": { key: "Slash", shift: true },
|
||||
"<": { key: "IntlBackslash" },
|
||||
">": { key: "IntlBackslash", shift: true },
|
||||
"|": { key: "IntlBackslash", altRight: true },
|
||||
" ": { key: "Space" },
|
||||
"\n": { key: "Enter" },
|
||||
Enter: { key: "Enter" },
|
||||
Tab: { key: "Tab" },
|
||||
} as Record<string, KeyCombo>;
|
|
@ -43,7 +43,7 @@ export const keys = {
|
|||
F13: 0x68,
|
||||
Home: 0x4a,
|
||||
Insert: 0x49,
|
||||
IntlBackslash: 0x31,
|
||||
IntlBackslash: 0x64,
|
||||
KeyA: 0x04,
|
||||
KeyB: 0x05,
|
||||
KeyC: 0x06,
|
||||
|
@ -104,116 +104,6 @@ export const keys = {
|
|||
Tab: 0x2b,
|
||||
} as Record<string, number>;
|
||||
|
||||
export const chars = {
|
||||
A: { key: "KeyA", shift: true },
|
||||
B: { key: "KeyB", shift: true },
|
||||
C: { key: "KeyC", shift: true },
|
||||
D: { key: "KeyD", shift: true },
|
||||
E: { key: "KeyE", shift: true },
|
||||
F: { key: "KeyF", shift: true },
|
||||
G: { key: "KeyG", shift: true },
|
||||
H: { key: "KeyH", shift: true },
|
||||
I: { key: "KeyI", shift: true },
|
||||
J: { key: "KeyJ", shift: true },
|
||||
K: { key: "KeyK", shift: true },
|
||||
L: { key: "KeyL", shift: true },
|
||||
M: { key: "KeyM", shift: true },
|
||||
N: { key: "KeyN", shift: true },
|
||||
O: { key: "KeyO", shift: true },
|
||||
P: { key: "KeyP", shift: true },
|
||||
Q: { key: "KeyQ", shift: true },
|
||||
R: { key: "KeyR", shift: true },
|
||||
S: { key: "KeyS", shift: true },
|
||||
T: { key: "KeyT", shift: true },
|
||||
U: { key: "KeyU", shift: true },
|
||||
V: { key: "KeyV", shift: true },
|
||||
W: { key: "KeyW", shift: true },
|
||||
X: { key: "KeyX", shift: true },
|
||||
Y: { key: "KeyY", shift: true },
|
||||
Z: { key: "KeyZ", shift: true },
|
||||
a: { key: "KeyA", shift: false },
|
||||
b: { key: "KeyB", shift: false },
|
||||
c: { key: "KeyC", shift: false },
|
||||
d: { key: "KeyD", shift: false },
|
||||
e: { key: "KeyE", shift: false },
|
||||
f: { key: "KeyF", shift: false },
|
||||
g: { key: "KeyG", shift: false },
|
||||
h: { key: "KeyH", shift: false },
|
||||
i: { key: "KeyI", shift: false },
|
||||
j: { key: "KeyJ", shift: false },
|
||||
k: { key: "KeyK", shift: false },
|
||||
l: { key: "KeyL", shift: false },
|
||||
m: { key: "KeyM", shift: false },
|
||||
n: { key: "KeyN", shift: false },
|
||||
o: { key: "KeyO", shift: false },
|
||||
p: { key: "KeyP", shift: false },
|
||||
q: { key: "KeyQ", shift: false },
|
||||
r: { key: "KeyR", shift: false },
|
||||
s: { key: "KeyS", shift: false },
|
||||
t: { key: "KeyT", shift: false },
|
||||
u: { key: "KeyU", shift: false },
|
||||
v: { key: "KeyV", shift: false },
|
||||
w: { key: "KeyW", shift: false },
|
||||
x: { key: "KeyX", shift: false },
|
||||
y: { key: "KeyY", shift: false },
|
||||
z: { key: "KeyZ", shift: false },
|
||||
1: { key: "Digit1", shift: false },
|
||||
"!": { key: "Digit1", shift: true },
|
||||
2: { key: "Digit2", shift: false },
|
||||
"@": { key: "Digit2", shift: true },
|
||||
3: { key: "Digit3", shift: false },
|
||||
"#": { key: "Digit3", shift: true },
|
||||
4: { key: "Digit4", shift: false },
|
||||
$: { key: "Digit4", shift: true },
|
||||
"%": { key: "Digit5", shift: true },
|
||||
5: { key: "Digit5", shift: false },
|
||||
"^": { key: "Digit6", shift: true },
|
||||
6: { key: "Digit6", shift: false },
|
||||
"&": { key: "Digit7", shift: true },
|
||||
7: { key: "Digit7", shift: false },
|
||||
"*": { key: "Digit8", shift: true },
|
||||
8: { key: "Digit8", shift: false },
|
||||
"(": { key: "Digit9", shift: true },
|
||||
9: { key: "Digit9", shift: false },
|
||||
")": { key: "Digit0", shift: true },
|
||||
0: { key: "Digit0", shift: false },
|
||||
"-": { key: "Minus", shift: false },
|
||||
_: { key: "Minus", shift: true },
|
||||
"=": { key: "Equal", shift: false },
|
||||
"+": { key: "Equal", shift: true },
|
||||
"'": { key: "Quote", shift: false },
|
||||
'"': { key: "Quote", shift: true },
|
||||
",": { key: "Comma", shift: false },
|
||||
"<": { key: "Comma", shift: true },
|
||||
"/": { key: "Slash", shift: false },
|
||||
"?": { key: "Slash", shift: true },
|
||||
".": { key: "Period", shift: false },
|
||||
">": { key: "Period", shift: true },
|
||||
";": { key: "Semicolon", shift: false },
|
||||
":": { key: "Semicolon", shift: true },
|
||||
"[": { key: "BracketLeft", shift: false },
|
||||
"{": { key: "BracketLeft", shift: true },
|
||||
"]": { key: "BracketRight", shift: false },
|
||||
"}": { key: "BracketRight", shift: true },
|
||||
"\\": { key: "Backslash", shift: false },
|
||||
"|": { key: "Backslash", shift: true },
|
||||
"`": { key: "Backquote", shift: false },
|
||||
"~": { key: "Backquote", shift: true },
|
||||
"§": { key: "IntlBackslash", shift: false },
|
||||
"±": { key: "IntlBackslash", shift: true },
|
||||
" ": { key: "Space", shift: false },
|
||||
"\n": { key: "Enter", shift: false },
|
||||
Enter: { key: "Enter", shift: false },
|
||||
Tab: { key: "Tab", shift: false },
|
||||
PrintScreen: { key: "Prt Sc", shift: false },
|
||||
SystemRequest: { key: "Prt Sc", shift: true },
|
||||
ScrollLock: { key: "ScrollLock", shift: false},
|
||||
Pause: { key: "Pause", shift: false },
|
||||
Break: { key: "Pause", shift: true },
|
||||
Insert: { key: "Insert", shift: false },
|
||||
Delete: { key: "Delete", shift: false },
|
||||
} as Record<string, { key: string | number; shift: boolean }>;
|
||||
|
||||
export const modifiers = {
|
||||
ControlLeft: 0x01,
|
||||
ControlRight: 0x10,
|
||||
|
|
|
@ -32,7 +32,8 @@ import { CLOUD_API, DEVICE_API } from "./ui.config";
|
|||
import OtherSessionRoute from "./routes/devices.$id.other-session";
|
||||
import MountRoute from "./routes/devices.$id.mount";
|
||||
import * as SettingsRoute from "./routes/devices.$id.settings";
|
||||
import SettingsKeyboardMouseRoute from "./routes/devices.$id.settings.mouse";
|
||||
import SettingsMouseRoute from "./routes/devices.$id.settings.mouse";
|
||||
import SettingsKeyboardRoute from "./routes/devices.$id.settings.keyboard";
|
||||
import api from "./api";
|
||||
import * as SettingsIndexRoute from "./routes/devices.$id.settings._index";
|
||||
import SettingsAdvancedRoute from "./routes/devices.$id.settings.advanced";
|
||||
|
@ -147,7 +148,11 @@ if (isOnDevice) {
|
|||
},
|
||||
{
|
||||
path: "mouse",
|
||||
element: <SettingsKeyboardMouseRoute />,
|
||||
element: <SettingsMouseRoute />,
|
||||
},
|
||||
{
|
||||
path: "keyboard",
|
||||
element: <SettingsKeyboardRoute />,
|
||||
},
|
||||
{
|
||||
path: "advanced",
|
||||
|
@ -276,7 +281,11 @@ if (isOnDevice) {
|
|||
},
|
||||
{
|
||||
path: "mouse",
|
||||
element: <SettingsKeyboardMouseRoute />,
|
||||
element: <SettingsMouseRoute />,
|
||||
},
|
||||
{
|
||||
path: "keyboard",
|
||||
element: <SettingsKeyboardRoute />,
|
||||
},
|
||||
{
|
||||
path: "advanced",
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
import { useCallback, useEffect } from "react";
|
||||
|
||||
import { useSettingsStore } from "@/hooks/stores";
|
||||
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import notifications from "@/notifications";
|
||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||
import { layouts } from "@/keyboardLayouts";
|
||||
|
||||
import { FeatureFlag } from "../components/FeatureFlag";
|
||||
import { SelectMenuBasic } from "../components/SelectMenuBasic";
|
||||
|
||||
import { SettingsItem } from "./devices.$id.settings";
|
||||
|
||||
export default function SettingsKeyboardRoute() {
|
||||
const keyboardLayout = useSettingsStore(state => state.keyboardLayout);
|
||||
const setKeyboardLayout = useSettingsStore(
|
||||
state => state.setKeyboardLayout,
|
||||
);
|
||||
|
||||
const layoutOptions = Object.entries(layouts).map(([code, language]) => { return { value: code, label: language } })
|
||||
|
||||
const [send] = useJsonRpc();
|
||||
|
||||
useEffect(() => {
|
||||
send("getKeyboardLayout", {}, resp => {
|
||||
if ("error" in resp) return;
|
||||
setKeyboardLayout(resp.result as string);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const onKeyboardLayoutChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const layout = e.target.value;
|
||||
send("setKeyboardLayout", { layout }, resp => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to set keyboard layout: ${resp.error.data || "Unknown error"}`,
|
||||
);
|
||||
}
|
||||
notifications.success("Keyboard layout set successfully");
|
||||
setKeyboardLayout(layout);
|
||||
});
|
||||
},
|
||||
[send, setKeyboardLayout],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<SettingsPageHeader
|
||||
title="Keyboard"
|
||||
description="Configure keyboard layout settings for your device"
|
||||
/>
|
||||
|
||||
<div className="space-y-4">
|
||||
<FeatureFlag minAppVersion="0.4.0" name="Paste text">
|
||||
{ /* this menu item could be renamed to plain "Keyboard layout" in the future, when also the virtual keyboard layout mappings are being implemented */ }
|
||||
<SettingsItem
|
||||
title="Paste text"
|
||||
description="Keyboard layout of target operating system"
|
||||
>
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
label=""
|
||||
fullWidth
|
||||
value={keyboardLayout}
|
||||
onChange={onKeyboardLayoutChange}
|
||||
options={layoutOptions}
|
||||
/>
|
||||
</SettingsItem>
|
||||
<p className="text-xs text-slate-600 dark:text-slate-400">
|
||||
Pasting text sends individual key strokes to the target device. The keyboard layout determines which key codes are being sent. Ensure that the keyboard layout in JetKVM matches the settings in the operating system.
|
||||
</p>
|
||||
</FeatureFlag>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,35 +1,27 @@
|
|||
import { CheckCircleIcon } from "@heroicons/react/16/solid";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import MouseIcon from "@/assets/mouse-icon.svg";
|
||||
import PointingFinger from "@/assets/pointing-finger.svg";
|
||||
import { GridCard } from "@/components/Card";
|
||||
import { Checkbox } from "@/components/Checkbox";
|
||||
import { useDeviceSettingsStore, useSettingsStore } from "@/hooks/stores";
|
||||
import { useSettingsStore } from "@/hooks/stores";
|
||||
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import notifications from "@/notifications";
|
||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||
|
||||
import { FeatureFlag } from "../components/FeatureFlag";
|
||||
import { SelectMenuBasic } from "../components/SelectMenuBasic";
|
||||
import { useFeatureFlag } from "../hooks/useFeatureFlag";
|
||||
import { cx } from "../cva.config";
|
||||
|
||||
import { SettingsItem } from "./devices.$id.settings";
|
||||
|
||||
type ScrollSensitivity = "low" | "default" | "high";
|
||||
|
||||
export default function SettingsKeyboardMouseRoute() {
|
||||
export default function SettingsMouseRoute() {
|
||||
const hideCursor = useSettingsStore(state => state.isCursorHidden);
|
||||
const setHideCursor = useSettingsStore(state => state.setCursorVisibility);
|
||||
|
||||
const mouseMode = useSettingsStore(state => state.mouseMode);
|
||||
const setMouseMode = useSettingsStore(state => state.setMouseMode);
|
||||
|
||||
const scrollSensitivity = useDeviceSettingsStore(state => state.scrollSensitivity);
|
||||
const setScrollSensitivity = useDeviceSettingsStore(
|
||||
state => state.setScrollSensitivity,
|
||||
);
|
||||
|
||||
const { isEnabled: isScrollSensitivityEnabled } = useFeatureFlag("0.3.8");
|
||||
|
||||
const [jiggler, setJiggler] = useState(false);
|
||||
|
@ -41,14 +33,7 @@ export default function SettingsKeyboardMouseRoute() {
|
|||
if ("error" in resp) return;
|
||||
setJiggler(resp.result as boolean);
|
||||
});
|
||||
|
||||
if (isScrollSensitivityEnabled) {
|
||||
send("getScrollSensitivity", {}, resp => {
|
||||
if ("error" in resp) return;
|
||||
setScrollSensitivity(resp.result as ScrollSensitivity);
|
||||
});
|
||||
}
|
||||
}, [isScrollSensitivityEnabled, send, setScrollSensitivity]);
|
||||
}, [isScrollSensitivityEnabled, send]);
|
||||
|
||||
const handleJigglerChange = (enabled: boolean) => {
|
||||
send("setJigglerState", { enabled }, resp => {
|
||||
|
@ -62,22 +47,6 @@ export default function SettingsKeyboardMouseRoute() {
|
|||
});
|
||||
};
|
||||
|
||||
const onScrollSensitivityChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const sensitivity = e.target.value as ScrollSensitivity;
|
||||
send("setScrollSensitivity", { sensitivity }, resp => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to set scroll sensitivity: ${resp.error.data || "Unknown error"}`,
|
||||
);
|
||||
}
|
||||
notifications.success("Scroll sensitivity set successfully");
|
||||
setScrollSensitivity(sensitivity);
|
||||
});
|
||||
},
|
||||
[send, setScrollSensitivity],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<SettingsPageHeader
|
||||
|
@ -96,28 +65,6 @@ export default function SettingsKeyboardMouseRoute() {
|
|||
/>
|
||||
</SettingsItem>
|
||||
|
||||
<FeatureFlag minAppVersion="0.3.8" name="Scroll Sensitivity">
|
||||
<SettingsItem
|
||||
title="Scroll Sensitivity"
|
||||
description="Adjust the scroll sensitivity"
|
||||
>
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
label=""
|
||||
fullWidth
|
||||
value={scrollSensitivity}
|
||||
onChange={onScrollSensitivityChange}
|
||||
options={
|
||||
[
|
||||
{ label: "Low", value: "low" },
|
||||
{ label: "Default", value: "default" },
|
||||
{ label: "High", value: "high" },
|
||||
] as { label: string; value: ScrollSensitivity }[]
|
||||
}
|
||||
/>
|
||||
</SettingsItem>
|
||||
</FeatureFlag>
|
||||
|
||||
<SettingsItem
|
||||
title="Jiggler"
|
||||
description="Simulate movement of a computer mouse. Prevents sleep mode, standby mode or the screensaver from activating"
|
||||
|
@ -131,17 +78,19 @@ export default function SettingsKeyboardMouseRoute() {
|
|||
<SettingsItem title="Modes" description="Choose the mouse input mode" />
|
||||
<div className="flex items-center gap-4">
|
||||
<button
|
||||
className="block group grow"
|
||||
onClick={() => { setMouseMode("absolute"); }}
|
||||
className="group block grow"
|
||||
onClick={() => {
|
||||
setMouseMode("absolute");
|
||||
}}
|
||||
>
|
||||
<GridCard>
|
||||
<div className="flex items-center px-4 py-3 group gap-x-4">
|
||||
<div className="group flex w-full items-center gap-x-4 px-4 py-3">
|
||||
<img
|
||||
className="w-6 shrink-0 dark:invert"
|
||||
src={PointingFinger}
|
||||
alt="Finger touching a screen"
|
||||
/>
|
||||
<div className="flex items-center justify-between grow">
|
||||
<div className="flex grow items-center justify-between">
|
||||
<div className="text-left">
|
||||
<h3 className="text-sm font-semibold text-black dark:text-white">
|
||||
Absolute
|
||||
|
@ -150,32 +99,44 @@ export default function SettingsKeyboardMouseRoute() {
|
|||
Most convenient
|
||||
</p>
|
||||
</div>
|
||||
{mouseMode === "absolute" && (
|
||||
<CheckCircleIcon className="w-4 h-4 text-blue-700 dark:text-blue-500" />
|
||||
)}
|
||||
<CheckCircleIcon
|
||||
className={cx(
|
||||
"h-4 w-4 text-blue-700 opacity-0 transition dark:text-blue-500",
|
||||
{ "opacity-100": mouseMode === "absolute" },
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</GridCard>
|
||||
</button>
|
||||
<button
|
||||
className="block group grow"
|
||||
onClick={() => { setMouseMode("relative"); }}
|
||||
className="group block grow"
|
||||
onClick={() => {
|
||||
setMouseMode("relative");
|
||||
}}
|
||||
>
|
||||
<GridCard>
|
||||
<div className="flex items-center px-4 py-3 gap-x-4">
|
||||
<img className="w-6 shrink-0 dark:invert" src={MouseIcon} alt="Mouse icon" />
|
||||
<div className="flex items-center justify-between grow">
|
||||
<div className="flex w-full items-center gap-x-4 px-4 py-3">
|
||||
<img
|
||||
className="w-6 shrink-0 dark:invert"
|
||||
src={MouseIcon}
|
||||
alt="Mouse icon"
|
||||
/>
|
||||
<div className="flex grow items-center justify-between">
|
||||
<div className="text-left">
|
||||
<h3 className="text-sm font-semibold text-black dark:text-white">
|
||||
Relative
|
||||
</h3>
|
||||
<p className="text-xs leading-none text-slate-800 dark:text-slate-300">
|
||||
Most Compatible (Beta)
|
||||
Most Compatible
|
||||
</p>
|
||||
</div>
|
||||
{mouseMode === "relative" && (
|
||||
<CheckCircleIcon className="w-4 h-4 text-blue-700 dark:text-blue-500" />
|
||||
)}
|
||||
<CheckCircleIcon
|
||||
className={cx(
|
||||
"h-4 w-4 text-blue-700 opacity-0 transition dark:text-blue-500",
|
||||
{ "opacity-100": mouseMode === "relative" },
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</GridCard>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { NavLink, Outlet, useLocation } from "react-router-dom";
|
||||
import {
|
||||
LuSettings,
|
||||
LuMouse,
|
||||
LuKeyboard,
|
||||
LuVideo,
|
||||
LuCpu,
|
||||
|
@ -149,11 +150,23 @@ export default function SettingsRoute() {
|
|||
className={({ isActive }) => (isActive ? "active" : "")}
|
||||
>
|
||||
<div className="flex items-center gap-x-2 rounded-md px-2.5 py-2.5 text-sm transition-colors hover:bg-slate-100 dark:hover:bg-slate-700 in-[.active]:bg-blue-50 in-[.active]:text-blue-700! md:in-[.active]:bg-transparent dark:in-[.active]:bg-blue-900 dark:in-[.active]:text-blue-200! dark:md:in-[.active]:bg-transparent">
|
||||
<LuKeyboard className="h-4 w-4 shrink-0" />
|
||||
|
||||
<LuMouse className="h-4 w-4 shrink-0" />
|
||||
<h1>Mouse</h1>
|
||||
</div>
|
||||
</NavLink>
|
||||
</div>
|
||||
<div className="shrink-0">
|
||||
<NavLink
|
||||
to="keyboard"
|
||||
className={({ isActive }) => (isActive ? "active" : "")}
|
||||
>
|
||||
<div className="flex items-center gap-x-2 rounded-md px-2.5 py-2.5 text-sm transition-colors hover:bg-slate-100 dark:hover:bg-slate-700 [.active_&]:bg-blue-50 [.active_&]:!text-blue-700 md:[.active_&]:bg-transparent dark:[.active_&]:bg-blue-900 dark:[.active_&]:!text-blue-200 dark:md:[.active_&]:bg-transparent">
|
||||
<LuKeyboard className="h-4 w-4 shrink-0" />
|
||||
<h1>Keyboard</h1>
|
||||
</div>
|
||||
</NavLink>
|
||||
</div>
|
||||
<div className="shrink-0">
|
||||
<NavLink
|
||||
to="video"
|
||||
|
|
|
@ -18,11 +18,9 @@ import useWebSocket from "react-use-websocket";
|
|||
|
||||
import { cx } from "@/cva.config";
|
||||
import {
|
||||
DeviceSettingsState,
|
||||
HidState,
|
||||
NetworkState,
|
||||
UpdateState,
|
||||
useDeviceSettingsStore,
|
||||
useDeviceStore,
|
||||
useHidStore,
|
||||
useMountMediaStore,
|
||||
|
@ -714,21 +712,6 @@ export default function KvmIdRoute() {
|
|||
});
|
||||
}, [appVersion, send, setAppVersion, setSystemVersion]);
|
||||
|
||||
const setScrollSensitivity = useDeviceSettingsStore(
|
||||
state => state.setScrollSensitivity,
|
||||
);
|
||||
|
||||
// Initialize device settings
|
||||
useEffect(
|
||||
function initializeDeviceSettings() {
|
||||
send("getScrollSensitivity", {}, resp => {
|
||||
if ("error" in resp) return;
|
||||
setScrollSensitivity(resp.result as DeviceSettingsState["scrollSensitivity"]);
|
||||
});
|
||||
},
|
||||
[send, setScrollSensitivity],
|
||||
);
|
||||
|
||||
const ConnectionStatusElement = useMemo(() => {
|
||||
const hasConnectionFailed =
|
||||
connectionFailed || ["failed", "closed"].includes(peerConnectionState || "");
|
||||
|
|
Loading…
Reference in New Issue