mirror of https://github.com/jetkvm/kvm.git
Merge remote-tracking branch 'upstream/dev' into feat/usb-audio
This commit is contained in:
commit
c529c903d0
|
@ -4,11 +4,24 @@
|
||||||
"features": {
|
"features": {
|
||||||
"ghcr.io/devcontainers/features/node:1": {
|
"ghcr.io/devcontainers/features/node:1": {
|
||||||
// Should match what is defined in ui/package.json
|
// Should match what is defined in ui/package.json
|
||||||
"version": "21.1.0"
|
"version": "22.15.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"mounts": [
|
"mounts": [
|
||||||
"source=${localEnv:HOME}/.ssh,target=/home/vscode/.ssh,type=bind,consistency=cached"
|
"source=${localEnv:HOME}/.ssh,target=/home/vscode/.ssh,type=bind,consistency=cached"
|
||||||
|
],
|
||||||
|
"customizations": {
|
||||||
|
"vscode": {
|
||||||
|
"extensions": [
|
||||||
|
"bradlc.vscode-tailwindcss",
|
||||||
|
"GitHub.vscode-pull-request-github",
|
||||||
|
"dbaeumer.vscode-eslint",
|
||||||
|
"golang.go",
|
||||||
|
"ms-vscode.makefile-tools",
|
||||||
|
"esbenp.prettier-vscode",
|
||||||
|
"github.vscode-github-actions"
|
||||||
]
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: gomod
|
||||||
|
directory: /
|
||||||
|
schedule:
|
||||||
|
interval: monthly
|
||||||
|
open-pull-requests-limit: 10
|
||||||
|
- package-ecosystem: github-actions
|
||||||
|
directory: /
|
||||||
|
schedule:
|
||||||
|
interval: monthly
|
||||||
|
open-pull-requests-limit: 10
|
||||||
|
- package-ecosystem: npm
|
||||||
|
directory: /ui
|
||||||
|
open-pull-requests-limit: 10
|
||||||
|
schedule:
|
||||||
|
interval: monthly
|
|
@ -19,21 +19,33 @@ jobs:
|
||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: v21.1.0
|
node-version: "22"
|
||||||
cache: "npm"
|
cache: "npm"
|
||||||
cache-dependency-path: "**/package-lock.json"
|
cache-dependency-path: "**/package-lock.json"
|
||||||
- name: Set up Golang
|
- name: Set up Golang
|
||||||
uses: actions/setup-go@v4
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: "1.24.0"
|
go-version: "1.24.4"
|
||||||
- name: Build frontend
|
- name: Build frontend
|
||||||
run: |
|
run: |
|
||||||
make frontend
|
make frontend
|
||||||
- name: Build application
|
- name: Build application
|
||||||
run: |
|
run: |
|
||||||
make build_dev
|
make build_dev
|
||||||
|
- 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:
|
||||||
|
input: "testreport.json"
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: jetkvm-app
|
name: jetkvm-app
|
||||||
path: bin/jetkvm_app
|
path: |
|
||||||
|
bin/jetkvm_app
|
||||||
|
device-tests.tar.gz
|
|
@ -24,9 +24,9 @@ jobs:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
|
uses: actions/setup-go@19bb51245e9c80abacb2e91cc42b33fa478b8639 # v4.2.1
|
||||||
with:
|
with:
|
||||||
go-version: 1.24.x
|
go-version: 1.24.4
|
||||||
- name: Create empty resource directory
|
- name: Create empty resource directory
|
||||||
run: |
|
run: |
|
||||||
mkdir -p static && touch static/.gitkeep
|
mkdir -p static && touch static/.gitkeep
|
||||||
|
|
|
@ -69,12 +69,54 @@ jobs:
|
||||||
CI_USER: ${{ vars.JETKVM_CI_USER }}
|
CI_USER: ${{ vars.JETKVM_CI_USER }}
|
||||||
CI_HOST: ${{ vars.JETKVM_CI_HOST }}
|
CI_HOST: ${{ vars.JETKVM_CI_HOST }}
|
||||||
CI_SSH_PRIVATE: ${{ secrets.JETKVM_CI_SSH_PRIVATE }}
|
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@v5
|
||||||
|
with:
|
||||||
|
go-version: "1.24.4"
|
||||||
|
- name: Golang Test Report
|
||||||
|
uses: becheran/go-testreport@v0.3.2
|
||||||
|
with:
|
||||||
|
input: "device-tests.json"
|
||||||
- name: Deploy application
|
- name: Deploy application
|
||||||
run: |
|
run: |
|
||||||
set -e
|
set -e
|
||||||
# Copy the binary to the remote host
|
# Copy the binary to the remote host
|
||||||
echo "+ Copying the application 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
|
# Deploy and run the application on the remote host
|
||||||
echo "+ Deploying the application on the remote host"
|
echo "+ Deploying the application on the remote host"
|
||||||
ssh jkci ash <<EOF
|
ssh jkci ash <<EOF
|
||||||
|
@ -108,15 +150,25 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
echo "+ Checking the status of the device"
|
echo "+ Checking the status of the device"
|
||||||
curl -v http://$CI_HOST/device/status && echo
|
curl -v http://$CI_HOST/device/status && echo
|
||||||
echo "+ Waiting for 10 seconds to allow all services to start"
|
echo "+ Waiting for 15 seconds to allow all services to start"
|
||||||
sleep 10
|
sleep 15
|
||||||
echo "+ Collecting logs"
|
echo "+ Collecting logs"
|
||||||
ssh jkci "cat /userdata/jetkvm/last.log" > last.log
|
local_log_tar=$(mktemp)
|
||||||
cat last.log
|
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:
|
env:
|
||||||
CI_HOST: ${{ vars.JETKVM_CI_HOST }}
|
CI_HOST: ${{ vars.JETKVM_CI_HOST }}
|
||||||
- name: Upload logs
|
- name: Upload logs
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: device-logs
|
name: device-logs
|
||||||
path: last.log
|
path: |
|
||||||
|
last.log
|
||||||
|
dmesg.log
|
||||||
|
device-tests.json
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
bin/*
|
bin/*
|
||||||
static/*
|
static/*
|
||||||
.idea
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
device-tests.tar.gz
|
|
@ -4,6 +4,7 @@ linters:
|
||||||
- forbidigo
|
- forbidigo
|
||||||
- misspell
|
- misspell
|
||||||
- whitespace
|
- whitespace
|
||||||
|
- gochecknoinits
|
||||||
settings:
|
settings:
|
||||||
forbidigo:
|
forbidigo:
|
||||||
forbid:
|
forbid:
|
||||||
|
@ -22,6 +23,9 @@ linters:
|
||||||
- linters:
|
- linters:
|
||||||
- errcheck
|
- errcheck
|
||||||
path: _test.go
|
path: _test.go
|
||||||
|
- linters:
|
||||||
|
- gochecknoinits
|
||||||
|
path: internal/logging/sse.go
|
||||||
paths:
|
paths:
|
||||||
- third_party$
|
- third_party$
|
||||||
- builtin$
|
- builtin$
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"tailwindCSS.classFunctions": ["cva", "cx"]
|
||||||
|
}
|
50
Makefile
50
Makefile
|
@ -2,12 +2,14 @@ BRANCH ?= $(shell git rev-parse --abbrev-ref HEAD)
|
||||||
BUILDDATE ?= $(shell date -u +%FT%T%z)
|
BUILDDATE ?= $(shell date -u +%FT%T%z)
|
||||||
BUILDTS ?= $(shell date -u +%s)
|
BUILDTS ?= $(shell date -u +%s)
|
||||||
REVISION ?= $(shell git rev-parse HEAD)
|
REVISION ?= $(shell git rev-parse HEAD)
|
||||||
VERSION_DEV := 0.4.0-dev$(shell date +%Y%m%d%H%M)
|
VERSION_DEV ?= 0.4.5-dev$(shell date +%Y%m%d%H%M)
|
||||||
VERSION := 0.3.9
|
VERSION ?= 0.4.4
|
||||||
|
|
||||||
PROMETHEUS_TAG := github.com/prometheus/common/version
|
PROMETHEUS_TAG := github.com/prometheus/common/version
|
||||||
KVM_PKG_NAME := github.com/jetkvm/kvm
|
KVM_PKG_NAME := github.com/jetkvm/kvm
|
||||||
|
|
||||||
|
GO_BUILD_ARGS := -tags netgo
|
||||||
|
GO_RELEASE_BUILD_ARGS := -trimpath $(GO_BUILD_ARGS)
|
||||||
GO_LDFLAGS := \
|
GO_LDFLAGS := \
|
||||||
-s -w \
|
-s -w \
|
||||||
-X $(PROMETHEUS_TAG).Branch=$(BRANCH) \
|
-X $(PROMETHEUS_TAG).Branch=$(BRANCH) \
|
||||||
|
@ -15,12 +17,49 @@ GO_LDFLAGS := \
|
||||||
-X $(PROMETHEUS_TAG).Revision=$(REVISION) \
|
-X $(PROMETHEUS_TAG).Revision=$(REVISION) \
|
||||||
-X $(KVM_PKG_NAME).builtTimestamp=$(BUILDTS)
|
-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:
|
hash_resource:
|
||||||
@shasum -a 256 resource/jetkvm_native | cut -d ' ' -f 1 > resource/jetkvm_native.sha256
|
@shasum -a 256 resource/jetkvm_native | cut -d ' ' -f 1 > resource/jetkvm_native.sha256
|
||||||
|
|
||||||
build_dev: hash_resource
|
build_dev: hash_resource
|
||||||
@echo "Building..."
|
@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)" \
|
||||||
|
$(GO_RELEASE_BUILD_ARGS) \
|
||||||
|
-o $(BIN_DIR)/jetkvm_app cmd/main.go
|
||||||
|
|
||||||
|
build_test2json:
|
||||||
|
$(GO_CMD) build -o $(BIN_DIR)/test2json cmd/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_DIR)/tests && mkdir -p $(BIN_DIR)/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; \
|
||||||
|
$(GO_CMD) test -v \
|
||||||
|
-ldflags="$(GO_LDFLAGS) -X $(KVM_PKG_NAME).builtAppVersion=$(VERSION_DEV)" \
|
||||||
|
$(GO_BUILD_ARGS) \
|
||||||
|
-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_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:
|
frontend:
|
||||||
cd ui && npm ci && npm run build:device
|
cd ui && npm ci && npm run build:device
|
||||||
|
@ -33,7 +72,10 @@ dev_release: frontend build_dev
|
||||||
|
|
||||||
build_release: frontend hash_resource
|
build_release: frontend hash_resource
|
||||||
@echo "Building release..."
|
@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)" \
|
||||||
|
$(GO_RELEASE_BUILD_ARGS) \
|
||||||
|
-o bin/jetkvm_app cmd/main.go
|
||||||
|
|
||||||
release:
|
release:
|
||||||
@if rclone lsf r2://jetkvm-update/app/$(VERSION)/ | grep -q "jetkvm_app"; then \
|
@if rclone lsf r2://jetkvm-update/app/$(VERSION)/ | grep -q "jetkvm_app"; then \
|
||||||
|
|
|
@ -7,6 +7,8 @@
|
||||||
|
|
||||||
[](https://twitter.com/jetkvm)
|
[](https://twitter.com/jetkvm)
|
||||||
|
|
||||||
|
[](https://goreportcard.com/report/github.com/jetkvm/kvm)
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
JetKVM is a high-performance, open-source KVM over IP (Keyboard, Video, Mouse) solution designed for efficient remote management of computers, servers, and workstations. Whether you're dealing with boot failures, installing a new operating system, adjusting BIOS settings, or simply taking control of a machine from afar, JetKVM provides the tools to get it done effectively.
|
JetKVM is a high-performance, open-source KVM over IP (Keyboard, Video, Mouse) solution designed for efficient remote management of computers, servers, and workstations. Whether you're dealing with boot failures, installing a new operating system, adjusting BIOS settings, or simply taking control of a machine from afar, JetKVM provides the tools to get it done effectively.
|
||||||
|
|
2
audio.go
2
audio.go
|
@ -53,7 +53,7 @@ func startFFmpeg() (cmd *exec.Cmd, err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func StartNtpAudioServer(handleClient func(net.Conn)) {
|
func StartRtpAudioServer(handleClient func(net.Conn)) {
|
||||||
scopedLogger := nativeLogger.With().
|
scopedLogger := nativeLogger.With().
|
||||||
Logger()
|
Logger()
|
||||||
|
|
||||||
|
|
22
cloud.go
22
cloud.go
|
@ -51,34 +51,34 @@ var (
|
||||||
)
|
)
|
||||||
metricCloudConnectionEstablishedTimestamp = promauto.NewGauge(
|
metricCloudConnectionEstablishedTimestamp = promauto.NewGauge(
|
||||||
prometheus.GaugeOpts{
|
prometheus.GaugeOpts{
|
||||||
Name: "jetkvm_cloud_connection_established_timestamp",
|
Name: "jetkvm_cloud_connection_established_timestamp_seconds",
|
||||||
Help: "The timestamp when the cloud connection was established",
|
Help: "The timestamp when the cloud connection was established",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
metricConnectionLastPingTimestamp = promauto.NewGaugeVec(
|
metricConnectionLastPingTimestamp = promauto.NewGaugeVec(
|
||||||
prometheus.GaugeOpts{
|
prometheus.GaugeOpts{
|
||||||
Name: "jetkvm_connection_last_ping_timestamp",
|
Name: "jetkvm_connection_last_ping_timestamp_seconds",
|
||||||
Help: "The timestamp when the last ping response was received",
|
Help: "The timestamp when the last ping response was received",
|
||||||
},
|
},
|
||||||
[]string{"type", "source"},
|
[]string{"type", "source"},
|
||||||
)
|
)
|
||||||
metricConnectionLastPingReceivedTimestamp = promauto.NewGaugeVec(
|
metricConnectionLastPingReceivedTimestamp = promauto.NewGaugeVec(
|
||||||
prometheus.GaugeOpts{
|
prometheus.GaugeOpts{
|
||||||
Name: "jetkvm_connection_last_ping_received_timestamp",
|
Name: "jetkvm_connection_last_ping_received_timestamp_seconds",
|
||||||
Help: "The timestamp when the last ping request was received",
|
Help: "The timestamp when the last ping request was received",
|
||||||
},
|
},
|
||||||
[]string{"type", "source"},
|
[]string{"type", "source"},
|
||||||
)
|
)
|
||||||
metricConnectionLastPingDuration = promauto.NewGaugeVec(
|
metricConnectionLastPingDuration = promauto.NewGaugeVec(
|
||||||
prometheus.GaugeOpts{
|
prometheus.GaugeOpts{
|
||||||
Name: "jetkvm_connection_last_ping_duration",
|
Name: "jetkvm_connection_last_ping_duration_seconds",
|
||||||
Help: "The duration of the last ping response",
|
Help: "The duration of the last ping response",
|
||||||
},
|
},
|
||||||
[]string{"type", "source"},
|
[]string{"type", "source"},
|
||||||
)
|
)
|
||||||
metricConnectionPingDuration = promauto.NewHistogramVec(
|
metricConnectionPingDuration = promauto.NewHistogramVec(
|
||||||
prometheus.HistogramOpts{
|
prometheus.HistogramOpts{
|
||||||
Name: "jetkvm_connection_ping_duration",
|
Name: "jetkvm_connection_ping_duration_seconds",
|
||||||
Help: "The duration of the ping response",
|
Help: "The duration of the ping response",
|
||||||
Buckets: []float64{
|
Buckets: []float64{
|
||||||
0.1, 0.5, 1, 10,
|
0.1, 0.5, 1, 10,
|
||||||
|
@ -88,28 +88,28 @@ var (
|
||||||
)
|
)
|
||||||
metricConnectionTotalPingSentCount = promauto.NewCounterVec(
|
metricConnectionTotalPingSentCount = promauto.NewCounterVec(
|
||||||
prometheus.CounterOpts{
|
prometheus.CounterOpts{
|
||||||
Name: "jetkvm_connection_total_ping_sent",
|
Name: "jetkvm_connection_ping_sent_total",
|
||||||
Help: "The total number of pings sent to the connection",
|
Help: "The total number of pings sent to the connection",
|
||||||
},
|
},
|
||||||
[]string{"type", "source"},
|
[]string{"type", "source"},
|
||||||
)
|
)
|
||||||
metricConnectionTotalPingReceivedCount = promauto.NewCounterVec(
|
metricConnectionTotalPingReceivedCount = promauto.NewCounterVec(
|
||||||
prometheus.CounterOpts{
|
prometheus.CounterOpts{
|
||||||
Name: "jetkvm_connection_total_ping_received",
|
Name: "jetkvm_connection_ping_received_total",
|
||||||
Help: "The total number of pings received from the connection",
|
Help: "The total number of pings received from the connection",
|
||||||
},
|
},
|
||||||
[]string{"type", "source"},
|
[]string{"type", "source"},
|
||||||
)
|
)
|
||||||
metricConnectionSessionRequestCount = promauto.NewCounterVec(
|
metricConnectionSessionRequestCount = promauto.NewCounterVec(
|
||||||
prometheus.CounterOpts{
|
prometheus.CounterOpts{
|
||||||
Name: "jetkvm_connection_session_total_requests",
|
Name: "jetkvm_connection_session_requests_total",
|
||||||
Help: "The total number of session requests received",
|
Help: "The total number of session requests received",
|
||||||
},
|
},
|
||||||
[]string{"type", "source"},
|
[]string{"type", "source"},
|
||||||
)
|
)
|
||||||
metricConnectionSessionRequestDuration = promauto.NewHistogramVec(
|
metricConnectionSessionRequestDuration = promauto.NewHistogramVec(
|
||||||
prometheus.HistogramOpts{
|
prometheus.HistogramOpts{
|
||||||
Name: "jetkvm_connection_session_request_duration",
|
Name: "jetkvm_connection_session_request_duration_seconds",
|
||||||
Help: "The duration of session requests",
|
Help: "The duration of session requests",
|
||||||
Buckets: []float64{
|
Buckets: []float64{
|
||||||
0.1, 0.5, 1, 10,
|
0.1, 0.5, 1, 10,
|
||||||
|
@ -119,7 +119,7 @@ var (
|
||||||
)
|
)
|
||||||
metricConnectionLastSessionRequestTimestamp = promauto.NewGaugeVec(
|
metricConnectionLastSessionRequestTimestamp = promauto.NewGaugeVec(
|
||||||
prometheus.GaugeOpts{
|
prometheus.GaugeOpts{
|
||||||
Name: "jetkvm_connection_last_session_request_timestamp",
|
Name: "jetkvm_connection_last_session_request_timestamp_seconds",
|
||||||
Help: "The timestamp of the last session request",
|
Help: "The timestamp of the last session request",
|
||||||
},
|
},
|
||||||
[]string{"type", "source"},
|
[]string{"type", "source"},
|
||||||
|
@ -133,7 +133,7 @@ var (
|
||||||
)
|
)
|
||||||
metricCloudConnectionFailureCount = promauto.NewCounter(
|
metricCloudConnectionFailureCount = promauto.NewCounter(
|
||||||
prometheus.CounterOpts{
|
prometheus.CounterOpts{
|
||||||
Name: "jetkvm_cloud_connection_failure_count",
|
Name: "jetkvm_cloud_connection_failure_total",
|
||||||
Help: "The number of times the cloud connection has failed",
|
Help: "The number of times the cloud connection has failed",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
@ -85,8 +85,10 @@ type Config struct {
|
||||||
HashedPassword string `json:"hashed_password"`
|
HashedPassword string `json:"hashed_password"`
|
||||||
LocalAuthToken string `json:"local_auth_token"`
|
LocalAuthToken string `json:"local_auth_token"`
|
||||||
LocalAuthMode string `json:"localAuthMode"` //TODO: fix it with migration
|
LocalAuthMode string `json:"localAuthMode"` //TODO: fix it with migration
|
||||||
|
LocalLoopbackOnly bool `json:"local_loopback_only"`
|
||||||
WakeOnLanDevices []WakeOnLanDevice `json:"wake_on_lan_devices"`
|
WakeOnLanDevices []WakeOnLanDevice `json:"wake_on_lan_devices"`
|
||||||
KeyboardMacros []KeyboardMacro `json:"keyboard_macros"`
|
KeyboardMacros []KeyboardMacro `json:"keyboard_macros"`
|
||||||
|
KeyboardLayout string `json:"keyboard_layout"`
|
||||||
EdidString string `json:"hdmi_edid_string"`
|
EdidString string `json:"hdmi_edid_string"`
|
||||||
ActiveExtension string `json:"active_extension"`
|
ActiveExtension string `json:"active_extension"`
|
||||||
DisplayRotation string `json:"display_rotation"`
|
DisplayRotation string `json:"display_rotation"`
|
||||||
|
@ -109,6 +111,7 @@ var defaultConfig = &Config{
|
||||||
ActiveExtension: "",
|
ActiveExtension: "",
|
||||||
KeyboardMacros: []KeyboardMacro{},
|
KeyboardMacros: []KeyboardMacro{},
|
||||||
DisplayRotation: "270",
|
DisplayRotation: "270",
|
||||||
|
KeyboardLayout: "en_US",
|
||||||
DisplayMaxBrightness: 64,
|
DisplayMaxBrightness: 64,
|
||||||
DisplayDimAfterSec: 120, // 2 minutes
|
DisplayDimAfterSec: 120, // 2 minutes
|
||||||
DisplayOffAfterSec: 1800, // 30 minutes
|
DisplayOffAfterSec: 1800, // 30 minutes
|
||||||
|
|
|
@ -3,6 +3,19 @@
|
||||||
# Exit immediately if a command exits with a non-zero status
|
# Exit immediately if a command exits with a non-zero status
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
|
C_RST="$(tput sgr0)"
|
||||||
|
C_ERR="$(tput setaf 1)"
|
||||||
|
C_OK="$(tput setaf 2)"
|
||||||
|
C_WARN="$(tput setaf 3)"
|
||||||
|
C_INFO="$(tput setaf 5)"
|
||||||
|
|
||||||
|
msg() { printf '%s%s%s\n' $2 "$1" $C_RST; }
|
||||||
|
|
||||||
|
msg_info() { msg "$1" $C_INFO; }
|
||||||
|
msg_ok() { msg "$1" $C_OK; }
|
||||||
|
msg_err() { msg "$1" $C_ERR; }
|
||||||
|
msg_warn() { msg "$1" $C_WARN; }
|
||||||
|
|
||||||
# Function to display help message
|
# Function to display help message
|
||||||
show_help() {
|
show_help() {
|
||||||
echo "Usage: $0 [options] -r <remote_ip>"
|
echo "Usage: $0 [options] -r <remote_ip>"
|
||||||
|
@ -12,6 +25,8 @@ show_help() {
|
||||||
echo
|
echo
|
||||||
echo "Optional:"
|
echo "Optional:"
|
||||||
echo " -u, --user <remote_user> Remote username (default: root)"
|
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 " --skip-ui-build Skip frontend/UI build"
|
||||||
echo " --help Display this help message"
|
echo " --help Display this help message"
|
||||||
echo
|
echo
|
||||||
|
@ -26,6 +41,8 @@ REMOTE_PATH="/userdata/jetkvm/bin"
|
||||||
SKIP_UI_BUILD=false
|
SKIP_UI_BUILD=false
|
||||||
RESET_USB_HID_DEVICE=false
|
RESET_USB_HID_DEVICE=false
|
||||||
LOG_TRACE_SCOPES="${LOG_TRACE_SCOPES:-jetkvm,cloud,websocket,native,jsonrpc}"
|
LOG_TRACE_SCOPES="${LOG_TRACE_SCOPES:-jetkvm,cloud,websocket,native,jsonrpc}"
|
||||||
|
RUN_GO_TESTS=false
|
||||||
|
RUN_GO_TESTS_ONLY=false
|
||||||
|
|
||||||
# Parse command line arguments
|
# Parse command line arguments
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
|
@ -46,6 +63,15 @@ while [[ $# -gt 0 ]]; do
|
||||||
RESET_USB_HID_DEVICE=true
|
RESET_USB_HID_DEVICE=true
|
||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
|
--run-go-tests)
|
||||||
|
RUN_GO_TESTS=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--run-go-tests-only)
|
||||||
|
RUN_GO_TESTS_ONLY=true
|
||||||
|
RUN_GO_TESTS=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
--help)
|
--help)
|
||||||
show_help
|
show_help
|
||||||
exit 0
|
exit 0
|
||||||
|
@ -60,26 +86,71 @@ done
|
||||||
|
|
||||||
# Verify required parameters
|
# Verify required parameters
|
||||||
if [ -z "$REMOTE_HOST" ]; then
|
if [ -z "$REMOTE_HOST" ]; then
|
||||||
echo "Error: Remote IP is a required parameter"
|
msg_err "Error: Remote IP is a required parameter"
|
||||||
show_help
|
show_help
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Build the development version on the host
|
# Build the development version on the host
|
||||||
if [ "$SKIP_UI_BUILD" = false ]; then
|
if [ "$SKIP_UI_BUILD" = false ]; then
|
||||||
|
msg_info "▶ Building frontend"
|
||||||
make frontend
|
make frontend
|
||||||
fi
|
fi
|
||||||
make build_dev
|
|
||||||
|
|
||||||
# Change directory to the binary output directory
|
if [ "$RUN_GO_TESTS" = true ]; then
|
||||||
cd bin
|
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 > /tmp/device-tests.tar.gz" < device-tests.tar.gz
|
||||||
|
|
||||||
|
msg_info "▶ Running go tests"
|
||||||
|
ssh "${REMOTE_USER}@${REMOTE_HOST}" 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
|
||||||
|
|
||||||
|
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
|
# Kill any existing instances of the application
|
||||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" "killall jetkvm_app_debug || true"
|
ssh "${REMOTE_USER}@${REMOTE_HOST}" "killall jetkvm_app_debug || true"
|
||||||
|
|
||||||
# Copy the binary to the remote host
|
# Copy the binary to the remote host
|
||||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" "cat > ${REMOTE_PATH}/jetkvm_app_debug" < jetkvm_app
|
ssh "${REMOTE_USER}@${REMOTE_HOST}" "cat > ${REMOTE_PATH}/jetkvm_app_debug" < bin/jetkvm_app
|
||||||
|
|
||||||
if [ "$RESET_USB_HID_DEVICE" = true ]; then
|
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
|
# 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}" "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"
|
ssh "${REMOTE_USER}@${REMOTE_HOST}" "ls /sys/class/udc > /sys/kernel/config/usb_gadget/jetkvm/UDC"
|
||||||
|
@ -103,7 +174,7 @@ cd "${REMOTE_PATH}"
|
||||||
chmod +x jetkvm_app_debug
|
chmod +x jetkvm_app_debug
|
||||||
|
|
||||||
# Run the application in the background
|
# Run the application in the background
|
||||||
PION_LOG_TRACE=${LOG_TRACE_SCOPES} ./jetkvm_app_debug
|
PION_LOG_TRACE=${LOG_TRACE_SCOPES} GODEBUG=netdns=1 ./jetkvm_app_debug
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
echo "Deployment complete."
|
echo "Deployment complete."
|
19
display.go
19
display.go
|
@ -339,10 +339,18 @@ func startBacklightTickers() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if dimTicker == nil && config.DisplayDimAfterSec != 0 {
|
// Stop existing tickers to prevent multiple active instances on repeated calls
|
||||||
|
if dimTicker != nil {
|
||||||
|
dimTicker.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
if offTicker != nil {
|
||||||
|
offTicker.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.DisplayDimAfterSec != 0 {
|
||||||
displayLogger.Info().Msg("dim_ticker has started")
|
displayLogger.Info().Msg("dim_ticker has started")
|
||||||
dimTicker = time.NewTicker(time.Duration(config.DisplayDimAfterSec) * time.Second)
|
dimTicker = time.NewTicker(time.Duration(config.DisplayDimAfterSec) * time.Second)
|
||||||
defer dimTicker.Stop()
|
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
for { //nolint:staticcheck
|
for { //nolint:staticcheck
|
||||||
|
@ -354,10 +362,9 @@ func startBacklightTickers() {
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
if offTicker == nil && config.DisplayOffAfterSec != 0 {
|
if config.DisplayOffAfterSec != 0 {
|
||||||
displayLogger.Info().Msg("off_ticker has started")
|
displayLogger.Info().Msg("off_ticker has started")
|
||||||
offTicker = time.NewTicker(time.Duration(config.DisplayOffAfterSec) * time.Second)
|
offTicker = time.NewTicker(time.Duration(config.DisplayOffAfterSec) * time.Second)
|
||||||
defer offTicker.Stop()
|
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
for { //nolint:staticcheck
|
for { //nolint:staticcheck
|
||||||
|
@ -370,9 +377,7 @@ func startBacklightTickers() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func initDisplay() {
|
||||||
ensureConfigLoaded()
|
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
waitCtrlClientConnected()
|
waitCtrlClientConnected()
|
||||||
displayLogger.Info().Msg("setting initial display contents")
|
displayLogger.Info().Msg("setting initial display contents")
|
||||||
|
|
69
go.mod
69
go.mod
|
@ -1,32 +1,38 @@
|
||||||
module github.com/jetkvm/kvm
|
module github.com/jetkvm/kvm
|
||||||
|
|
||||||
go 1.23.0
|
go 1.23.4
|
||||||
|
|
||||||
|
toolchain go1.24.3
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Masterminds/semver/v3 v3.3.0
|
github.com/Masterminds/semver/v3 v3.3.1
|
||||||
github.com/beevik/ntp v1.3.1
|
github.com/beevik/ntp v1.4.3
|
||||||
github.com/coder/websocket v1.8.13
|
github.com/coder/websocket v1.8.13
|
||||||
github.com/coreos/go-oidc/v3 v3.11.0
|
github.com/coreos/go-oidc/v3 v3.11.0
|
||||||
github.com/creack/pty v1.1.23
|
github.com/creack/pty v1.1.23
|
||||||
github.com/fsnotify/fsnotify v1.9.0
|
github.com/fsnotify/fsnotify v1.9.0
|
||||||
github.com/gin-contrib/logger v1.2.5
|
github.com/gin-contrib/logger v1.2.6
|
||||||
github.com/gin-gonic/gin v1.10.0
|
github.com/gin-gonic/gin v1.10.1
|
||||||
github.com/google/uuid v1.6.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/gwatts/rootcerts v0.0.0-20240401182218-3ab9db955caf
|
||||||
github.com/hanwen/go-fuse/v2 v2.5.1
|
github.com/hanwen/go-fuse/v2 v2.8.0
|
||||||
github.com/hashicorp/go-envparse v0.1.0
|
github.com/pion/logging v0.2.3
|
||||||
github.com/pion/logging v0.2.2
|
|
||||||
github.com/pion/mdns/v2 v2.0.7
|
github.com/pion/mdns/v2 v2.0.7
|
||||||
github.com/pion/webrtc/v4 v4.0.0
|
github.com/pion/webrtc/v4 v4.0.16
|
||||||
github.com/pojntfx/go-nbd v0.3.2
|
github.com/pojntfx/go-nbd v0.3.2
|
||||||
github.com/prometheus/client_golang v1.21.0
|
github.com/prometheus/client_golang v1.22.0
|
||||||
github.com/prometheus/common v0.62.0
|
github.com/prometheus/common v0.62.0
|
||||||
|
github.com/prometheus/procfs v0.16.1
|
||||||
github.com/psanford/httpreadat v0.1.0
|
github.com/psanford/httpreadat v0.1.0
|
||||||
github.com/rs/zerolog v1.34.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
|
github.com/vishvananda/netlink v1.3.0
|
||||||
go.bug.st/serial v1.6.2
|
go.bug.st/serial v1.6.2
|
||||||
golang.org/x/crypto v0.36.0
|
golang.org/x/crypto v0.39.0
|
||||||
golang.org/x/net v0.38.0
|
golang.org/x/net v0.41.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
|
replace github.com/pojntfx/go-nbd v0.3.2 => github.com/chemhack/go-nbd v0.0.0-20241006125820-59e45f5b1e7b
|
||||||
|
@ -38,16 +44,15 @@ require (
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.5 // indirect
|
github.com/cloudwego/base64x v0.1.5 // indirect
|
||||||
github.com/creack/goselect v0.1.2 // indirect
|
github.com/creack/goselect v0.1.2 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/gin-contrib/sse v1.0.0 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
|
||||||
github.com/go-jose/go-jose/v4 v4.0.2 // indirect
|
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||||
|
github.com/go-jose/go-jose/v4 v4.0.5 // indirect
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/go-playground/validator/v10 v10.26.0 // indirect
|
github.com/go-playground/validator/v10 v10.26.0 // indirect
|
||||||
github.com/goccy/go-json v0.10.5 // indirect
|
github.com/goccy/go-json v0.10.5 // indirect
|
||||||
github.com/guregu/null/v6 v6.0.0 // indirect
|
|
||||||
github.com/json-iterator/go v1.1.12 // 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
|
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
|
@ -55,31 +60,31 @@ require (
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
github.com/pilebones/go-udev v0.9.0 // indirect
|
github.com/pilebones/go-udev v0.9.0 // indirect
|
||||||
github.com/pion/datachannel v1.5.9 // indirect
|
github.com/pion/datachannel v1.5.10 // indirect
|
||||||
github.com/pion/dtls/v3 v3.0.3 // indirect
|
github.com/pion/dtls/v3 v3.0.6 // indirect
|
||||||
github.com/pion/ice/v4 v4.0.2 // indirect
|
github.com/pion/ice/v4 v4.0.10 // indirect
|
||||||
github.com/pion/interceptor v0.1.37 // indirect
|
github.com/pion/interceptor v0.1.40 // indirect
|
||||||
github.com/pion/randutil v0.1.0 // indirect
|
github.com/pion/randutil v0.1.0 // indirect
|
||||||
github.com/pion/rtcp v1.2.14 // indirect
|
github.com/pion/rtcp v1.2.15 // indirect
|
||||||
github.com/pion/rtp v1.8.9 // indirect
|
github.com/pion/rtp v1.8.18 // indirect
|
||||||
github.com/pion/sctp v1.8.33 // indirect
|
github.com/pion/sctp v1.8.39 // indirect
|
||||||
github.com/pion/sdp/v3 v3.0.9 // indirect
|
github.com/pion/sdp/v3 v3.0.13 // indirect
|
||||||
github.com/pion/srtp/v3 v3.0.4 // indirect
|
github.com/pion/srtp/v3 v3.0.5 // indirect
|
||||||
github.com/pion/stun/v3 v3.0.0 // indirect
|
github.com/pion/stun/v3 v3.0.0 // indirect
|
||||||
github.com/pion/transport/v3 v3.0.7 // indirect
|
github.com/pion/transport/v3 v3.0.7 // indirect
|
||||||
github.com/pion/turn/v4 v4.0.0 // indirect
|
github.com/pion/turn/v4 v4.0.2 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/prometheus/client_model v0.6.1 // 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/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||||
github.com/vishvananda/netns v0.0.4 // indirect
|
github.com/vishvananda/netns v0.0.4 // indirect
|
||||||
github.com/wlynxg/anet v0.0.5 // indirect
|
github.com/wlynxg/anet v0.0.5 // indirect
|
||||||
golang.org/x/arch v0.15.0 // indirect
|
golang.org/x/arch v0.17.0 // indirect
|
||||||
golang.org/x/oauth2 v0.24.0 // indirect
|
golang.org/x/oauth2 v0.24.0 // indirect
|
||||||
golang.org/x/sys v0.32.0 // indirect
|
golang.org/x/text v0.26.0 // indirect
|
||||||
golang.org/x/text v0.23.0 // indirect
|
|
||||||
google.golang.org/protobuf v1.36.6 // indirect
|
google.golang.org/protobuf v1.36.6 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
138
go.sum
138
go.sum
|
@ -1,7 +1,7 @@
|
||||||
github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0=
|
github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4=
|
||||||
github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||||
github.com/beevik/ntp v1.3.1 h1:Y/srlT8L1yQr58kyPWFPZIxRL8ttx2SRIpVYJqZIlAM=
|
github.com/beevik/ntp v1.4.3 h1:PlbTvE5NNy4QHmA4Mg57n7mcFTmr1W1j3gcK7L1lqho=
|
||||||
github.com/beevik/ntp v1.3.1/go.mod h1:fT6PylBq86Tsq23ZMEe47b7QQrZfYBFPnpzt0a9kJxw=
|
github.com/beevik/ntp v1.4.3/go.mod h1:Unr8Zg+2dRn7d8bHFuehIMSvvUYssHMxW3Q5Nx4RW5Q=
|
||||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ=
|
github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ=
|
||||||
|
@ -30,16 +30,16 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
|
||||||
github.com/gin-contrib/logger v1.2.5 h1:qVQI4omayQecuN4zX9ZZnsOq7w9J/ZLds3J/FMn8ypM=
|
github.com/gin-contrib/logger v1.2.6 h1:EPolruKUTzNXMVBD9LuAFQmRjTs7AH7yKGuXgYqrKWc=
|
||||||
github.com/gin-contrib/logger v1.2.5/go.mod h1:/bj+vNMuA2xOEQ1aRHoJ1m9+uyaaXIAxQTvM2llsc6I=
|
github.com/gin-contrib/logger v1.2.6/go.mod h1:7niPrd7F0Nscw/zvgz8RiGJxSdbKM2yfQNy8xCHcm64=
|
||||||
github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E=
|
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||||
github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0=
|
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||||
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
|
||||||
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||||
github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk=
|
github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
|
||||||
github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY=
|
github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
|
||||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
|
@ -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 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
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/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.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
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/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 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
@ -60,14 +60,12 @@ github.com/guregu/null/v6 v6.0.0 h1:N14VRS+4di81i1PXRiprbQJ9EM9gqBa0+KVMeS/QSjQ=
|
||||||
github.com/guregu/null/v6 v6.0.0/go.mod h1:hrMIhIfrOZeLPZhROSn149tpw2gHkidAqxoXNyeX3iQ=
|
github.com/guregu/null/v6 v6.0.0/go.mod h1:hrMIhIfrOZeLPZhROSn149tpw2gHkidAqxoXNyeX3iQ=
|
||||||
github.com/gwatts/rootcerts v0.0.0-20240401182218-3ab9db955caf h1:JO6ISZIvEUitto5zjQ3/VEnDM5rPbqIFuOhS0U0ByeA=
|
github.com/gwatts/rootcerts v0.0.0-20240401182218-3ab9db955caf h1:JO6ISZIvEUitto5zjQ3/VEnDM5rPbqIFuOhS0U0ByeA=
|
||||||
github.com/gwatts/rootcerts v0.0.0-20240401182218-3ab9db955caf/go.mod h1:5Kt9XkWvkGi2OHOq0QsGxebHmhCcqJ8KCbNg/a6+n+g=
|
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.8.0 h1:wV8rG7rmCz8XHSOwBZhG5YcVqcYjkzivjmbaMafPlAs=
|
||||||
github.com/hanwen/go-fuse/v2 v2.5.1/go.mod h1:xKwi1cF7nXAOBCXujD5ie0ZKsxc8GGSA1rlMJc+8IJs=
|
github.com/hanwen/go-fuse/v2 v2.8.0/go.mod h1:yE6D2PqWwm3CbYRxFXV9xUd8Md5d6NG0WBs5spCswmI=
|
||||||
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 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
|
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||||
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
|
@ -79,7 +77,6 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k=
|
|
||||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
|
@ -91,8 +88,8 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
|
||||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78=
|
github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg=
|
||||||
github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI=
|
github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
@ -100,71 +97,70 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
github.com/pilebones/go-udev v0.9.0 h1:N1uEO/SxUwtIctc0WLU0t69JeBxIYEYnj8lT/Nabl9Q=
|
github.com/pilebones/go-udev v0.9.0 h1:N1uEO/SxUwtIctc0WLU0t69JeBxIYEYnj8lT/Nabl9Q=
|
||||||
github.com/pilebones/go-udev v0.9.0/go.mod h1:T2eI2tUSK0hA2WS5QLjXJUfQkluZQu+18Cqvem3CaXI=
|
github.com/pilebones/go-udev v0.9.0/go.mod h1:T2eI2tUSK0hA2WS5QLjXJUfQkluZQu+18Cqvem3CaXI=
|
||||||
github.com/pion/datachannel v1.5.9 h1:LpIWAOYPyDrXtU+BW7X0Yt/vGtYxtXQ8ql7dFfYUVZA=
|
github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o=
|
||||||
github.com/pion/datachannel v1.5.9/go.mod h1:kDUuk4CU4Uxp82NH4LQZbISULkX/HtzKa4P7ldf9izE=
|
github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M=
|
||||||
github.com/pion/dtls/v3 v3.0.3 h1:j5ajZbQwff7Z8k3pE3S+rQ4STvKvXUdKsi/07ka+OWM=
|
github.com/pion/dtls/v3 v3.0.6 h1:7Hkd8WhAJNbRgq9RgdNh1aaWlZlGpYTzdqjy9x9sK2E=
|
||||||
github.com/pion/dtls/v3 v3.0.3/go.mod h1:weOTUyIV4z0bQaVzKe8kpaP17+us3yAuiQsEAG1STMU=
|
github.com/pion/dtls/v3 v3.0.6/go.mod h1:iJxNQ3Uhn1NZWOMWlLxEEHAN5yX7GyPvvKw04v9bzYU=
|
||||||
github.com/pion/ice/v4 v4.0.2 h1:1JhBRX8iQLi0+TfcavTjPjI6GO41MFn4CeTBX+Y9h5s=
|
github.com/pion/ice/v4 v4.0.10 h1:P59w1iauC/wPk9PdY8Vjl4fOFL5B+USq1+xbDcN6gT4=
|
||||||
github.com/pion/ice/v4 v4.0.2/go.mod h1:DCdqyzgtsDNYN6/3U8044j3U7qsJ9KFJC92VnOWHvXg=
|
github.com/pion/ice/v4 v4.0.10/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw=
|
||||||
github.com/pion/interceptor v0.1.37 h1:aRA8Zpab/wE7/c0O3fh1PqY0AJI3fCSEM5lRWJVorwI=
|
github.com/pion/interceptor v0.1.40 h1:e0BjnPcGpr2CFQgKhrQisBU7V3GXK6wrfYrGYaU6Jq4=
|
||||||
github.com/pion/interceptor v0.1.37/go.mod h1:JzxbJ4umVTlZAf+/utHzNesY8tmRkM2lVmkS82TTj8Y=
|
github.com/pion/interceptor v0.1.40/go.mod h1:Z6kqH7M/FYirg3frjGJ21VLSRJGBXB/KqaTIrdqnOic=
|
||||||
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
|
github.com/pion/logging v0.2.3 h1:gHuf0zpoh1GW67Nr6Gj4cv5Z9ZscU7g/EaoC/Ke/igI=
|
||||||
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
|
github.com/pion/logging v0.2.3/go.mod h1:z8YfknkquMe1csOrxK5kc+5/ZPAzMxbKLX5aXpbpC90=
|
||||||
github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM=
|
github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM=
|
||||||
github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA=
|
github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA=
|
||||||
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
||||||
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
||||||
github.com/pion/rtcp v1.2.14 h1:KCkGV3vJ+4DAJmvP0vaQShsb0xkRfWkO540Gy102KyE=
|
github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo=
|
||||||
github.com/pion/rtcp v1.2.14/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
|
github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0=
|
||||||
github.com/pion/rtp v1.8.9 h1:E2HX740TZKaqdcPmf4pw6ZZuG8u5RlMMt+l3dxeu6Wk=
|
github.com/pion/rtp v1.8.18 h1:yEAb4+4a8nkPCecWzQB6V/uEU18X1lQCGAQCjP+pyvU=
|
||||||
github.com/pion/rtp v1.8.9/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
|
github.com/pion/rtp v1.8.18/go.mod h1:bAu2UFKScgzyFqvUKmbvzSdPr+NGbZtv6UB2hesqXBk=
|
||||||
github.com/pion/sctp v1.8.33 h1:dSE4wX6uTJBcNm8+YlMg7lw1wqyKHggsP5uKbdj+NZw=
|
github.com/pion/sctp v1.8.39 h1:PJma40vRHa3UTO3C4MyeJDQ+KIobVYRZQZ0Nt7SjQnE=
|
||||||
github.com/pion/sctp v1.8.33/go.mod h1:beTnqSzewI53KWoG3nqB282oDMGrhNxBdb+JZnkCwRM=
|
github.com/pion/sctp v1.8.39/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE=
|
||||||
github.com/pion/sdp/v3 v3.0.9 h1:pX++dCHoHUwq43kuwf3PyJfHlwIj4hXA7Vrifiq0IJY=
|
github.com/pion/sdp/v3 v3.0.13 h1:uN3SS2b+QDZnWXgdr69SM8KB4EbcnPnPf2Laxhty/l4=
|
||||||
github.com/pion/sdp/v3 v3.0.9/go.mod h1:B5xmvENq5IXJimIO4zfp6LAe1fD9N+kFv+V/1lOdz8M=
|
github.com/pion/sdp/v3 v3.0.13/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E=
|
||||||
github.com/pion/srtp/v3 v3.0.4 h1:2Z6vDVxzrX3UHEgrUyIGM4rRouoC7v+NiF1IHtp9B5M=
|
github.com/pion/srtp/v3 v3.0.5 h1:8XLB6Dt3QXkMkRFpoqC3314BemkpMQK2mZeJc4pUKqo=
|
||||||
github.com/pion/srtp/v3 v3.0.4/go.mod h1:1Jx3FwDoxpRaTh1oRV8A/6G1BnFL+QI82eK4ms8EEJQ=
|
github.com/pion/srtp/v3 v3.0.5/go.mod h1:r1G7y5r1scZRLe2QJI/is+/O83W2d+JoEsuIexpw+uM=
|
||||||
github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw=
|
github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw=
|
||||||
github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU=
|
github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU=
|
||||||
github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0=
|
github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0=
|
||||||
github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo=
|
github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo=
|
||||||
github.com/pion/turn/v4 v4.0.0 h1:qxplo3Rxa9Yg1xXDxxH8xaqcyGUtbHYw4QSCvmFWvhM=
|
github.com/pion/turn/v4 v4.0.2 h1:ZqgQ3+MjP32ug30xAbD6Mn+/K4Sxi3SdNOTFf+7mpps=
|
||||||
github.com/pion/turn/v4 v4.0.0/go.mod h1:MuPDkm15nYSklKpN8vWJ9W2M0PlyQZqYt1McGuxG7mA=
|
github.com/pion/turn/v4 v4.0.2/go.mod h1:pMMKP/ieNAG/fN5cZiN4SDuyKsXtNTr0ccN7IToA1zs=
|
||||||
github.com/pion/webrtc/v4 v4.0.0 h1:x8ec7uJQPP3D1iI8ojPAiTOylPI7Fa7QgqZrhpLyqZ8=
|
github.com/pion/webrtc/v4 v4.0.16 h1:5f8QMVIbNvJr2mPRGi2QamkPa/LVUB6NWolOCwphKHA=
|
||||||
github.com/pion/webrtc/v4 v4.0.0/go.mod h1:SfNn8CcFxR6OUVjLXVslAQ3a3994JhyE3Hw1jAuqEto=
|
github.com/pion/webrtc/v4 v4.0.16/go.mod h1:C3uTCPzVafUA0eUzru9f47OgNt3nEO7ZJ6zNY6VSJno=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/prometheus/client_golang v1.21.0 h1:DIsaGmiaBkSangBgMtWdNfxbMNdku5IK6iNhrEqWvdA=
|
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
|
||||||
github.com/prometheus/client_golang v1.21.0/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg=
|
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
|
||||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||||
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
|
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
|
||||||
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
|
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
|
||||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
|
||||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
|
||||||
github.com/psanford/httpreadat v0.1.0 h1:VleW1HS2zO7/4c7c7zNl33fO6oYACSagjJIyMIwZLUE=
|
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/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.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
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/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 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||||
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
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.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.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
|
||||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
|
@ -179,25 +175,23 @@ github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
|
||||||
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
|
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
|
||||||
go.bug.st/serial v1.6.2 h1:kn9LRX3sdm+WxWKufMlIRndwGfPWsH1/9lCWXQCasq8=
|
go.bug.st/serial v1.6.2 h1:kn9LRX3sdm+WxWKufMlIRndwGfPWsH1/9lCWXQCasq8=
|
||||||
go.bug.st/serial v1.6.2/go.mod h1:UABfsluHAiaNI+La2iESysd9Vetq7VRdpxvjx7CmmOE=
|
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.17.0 h1:4O3dfLzd+lQewptAHqjewQZQDyEdejz3VwgeYwkZneU=
|
||||||
golang.org/x/arch v0.15.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE=
|
golang.org/x/arch v0.17.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
||||||
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
||||||
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
||||||
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
||||||
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
||||||
golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
|
golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
|
||||||
golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
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=
|
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.10.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.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||||
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
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=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
|
|
@ -57,7 +57,22 @@ func TestValidateConfig(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestValidateIPv4StaticConfigRequired(t *testing.T) {
|
func TestValidateIPv4StaticConfigNetmaskRequiredIfStatic(t *testing.T) {
|
||||||
|
config := &testNetworkConfig{
|
||||||
|
IPv4Static: &testIPv4StaticConfig{
|
||||||
|
Address: null.StringFrom("192.168.1.1"),
|
||||||
|
Gateway: null.StringFrom("192.168.1.1"),
|
||||||
|
},
|
||||||
|
IPv4Mode: null.StringFrom("static"),
|
||||||
|
}
|
||||||
|
|
||||||
|
err := SetDefaultsAndValidate(config)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("expected error, got nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateIPv4StaticConfigNetmaskNotRequiredIfStatic(t *testing.T) {
|
||||||
config := &testNetworkConfig{
|
config := &testNetworkConfig{
|
||||||
IPv4Static: &testIPv4StaticConfig{
|
IPv4Static: &testIPv4StaticConfig{
|
||||||
Address: null.StringFrom("192.168.1.1"),
|
Address: null.StringFrom("192.168.1.1"),
|
||||||
|
@ -66,8 +81,8 @@ func TestValidateIPv4StaticConfigRequired(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
err := SetDefaultsAndValidate(config)
|
err := SetDefaultsAndValidate(config)
|
||||||
if err == nil {
|
if err != nil {
|
||||||
t.Fatalf("expected error, got nil")
|
t.Fatalf("expected no error, got %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -95,16 +95,27 @@ func (t *TimeSync) queryMultipleHttp(urls []string, timeout time.Duration) (now
|
||||||
} else if errors.Is(err, context.Canceled) {
|
} else if errors.Is(err, context.Canceled) {
|
||||||
metricHttpCancelCount.WithLabelValues(url).Inc()
|
metricHttpCancelCount.WithLabelValues(url).Inc()
|
||||||
metricHttpTotalCancelCount.Inc()
|
metricHttpTotalCancelCount.Inc()
|
||||||
|
results <- nil
|
||||||
} else {
|
} else {
|
||||||
scopedLogger.Warn().
|
scopedLogger.Warn().
|
||||||
Str("error", err.Error()).
|
Str("error", err.Error()).
|
||||||
Int("status", status).
|
Int("status", status).
|
||||||
Msg("failed to query HTTP server")
|
Msg("failed to query HTTP server")
|
||||||
|
results <- nil
|
||||||
}
|
}
|
||||||
}(url)
|
}(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
return <-results
|
for range urls {
|
||||||
|
result := <-results
|
||||||
|
if result == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
now = result
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func queryHttpTime(
|
func queryHttpTime(
|
||||||
|
|
|
@ -14,44 +14,44 @@ var (
|
||||||
)
|
)
|
||||||
metricTimeSyncCount = promauto.NewCounter(
|
metricTimeSyncCount = promauto.NewCounter(
|
||||||
prometheus.CounterOpts{
|
prometheus.CounterOpts{
|
||||||
Name: "jetkvm_timesync_count",
|
Name: "jetkvm_timesync_total",
|
||||||
Help: "The number of times the timesync has been run",
|
Help: "The number of times the timesync has been run",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
metricTimeSyncSuccessCount = promauto.NewCounter(
|
metricTimeSyncSuccessCount = promauto.NewCounter(
|
||||||
prometheus.CounterOpts{
|
prometheus.CounterOpts{
|
||||||
Name: "jetkvm_timesync_success_count",
|
Name: "jetkvm_timesync_success_total",
|
||||||
Help: "The number of times the timesync has been successful",
|
Help: "The number of times the timesync has been successful",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
metricRTCUpdateCount = promauto.NewCounter( //nolint:unused
|
metricRTCUpdateCount = promauto.NewCounter( //nolint:unused
|
||||||
prometheus.CounterOpts{
|
prometheus.CounterOpts{
|
||||||
Name: "jetkvm_timesync_rtc_update_count",
|
Name: "jetkvm_timesync_rtc_update_total",
|
||||||
Help: "The number of times the RTC has been updated",
|
Help: "The number of times the RTC has been updated",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
metricNtpTotalSuccessCount = promauto.NewCounter(
|
metricNtpTotalSuccessCount = promauto.NewCounter(
|
||||||
prometheus.CounterOpts{
|
prometheus.CounterOpts{
|
||||||
Name: "jetkvm_timesync_ntp_total_success_count",
|
Name: "jetkvm_timesync_ntp_total_success_total",
|
||||||
Help: "The total number of successful NTP requests",
|
Help: "The total number of successful NTP requests",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
metricNtpTotalRequestCount = promauto.NewCounter(
|
metricNtpTotalRequestCount = promauto.NewCounter(
|
||||||
prometheus.CounterOpts{
|
prometheus.CounterOpts{
|
||||||
Name: "jetkvm_timesync_ntp_total_request_count",
|
Name: "jetkvm_timesync_ntp_total_request_total",
|
||||||
Help: "The total number of NTP requests sent",
|
Help: "The total number of NTP requests sent",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
metricNtpSuccessCount = promauto.NewCounterVec(
|
metricNtpSuccessCount = promauto.NewCounterVec(
|
||||||
prometheus.CounterOpts{
|
prometheus.CounterOpts{
|
||||||
Name: "jetkvm_timesync_ntp_success_count",
|
Name: "jetkvm_timesync_ntp_success_total",
|
||||||
Help: "The number of successful NTP requests",
|
Help: "The number of successful NTP requests",
|
||||||
},
|
},
|
||||||
[]string{"url"},
|
[]string{"url"},
|
||||||
)
|
)
|
||||||
metricNtpRequestCount = promauto.NewCounterVec(
|
metricNtpRequestCount = promauto.NewCounterVec(
|
||||||
prometheus.CounterOpts{
|
prometheus.CounterOpts{
|
||||||
Name: "jetkvm_timesync_ntp_request_count",
|
Name: "jetkvm_timesync_ntp_request_total",
|
||||||
Help: "The number of NTP requests sent to the server",
|
Help: "The number of NTP requests sent to the server",
|
||||||
},
|
},
|
||||||
[]string{"url"},
|
[]string{"url"},
|
||||||
|
@ -83,39 +83,39 @@ var (
|
||||||
|
|
||||||
metricHttpTotalSuccessCount = promauto.NewCounter(
|
metricHttpTotalSuccessCount = promauto.NewCounter(
|
||||||
prometheus.CounterOpts{
|
prometheus.CounterOpts{
|
||||||
Name: "jetkvm_timesync_http_total_success_count",
|
Name: "jetkvm_timesync_http_total_success_total",
|
||||||
Help: "The total number of successful HTTP requests",
|
Help: "The total number of successful HTTP requests",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
metricHttpTotalRequestCount = promauto.NewCounter(
|
metricHttpTotalRequestCount = promauto.NewCounter(
|
||||||
prometheus.CounterOpts{
|
prometheus.CounterOpts{
|
||||||
Name: "jetkvm_timesync_http_total_request_count",
|
Name: "jetkvm_timesync_http_total_request_total",
|
||||||
Help: "The total number of HTTP requests sent",
|
Help: "The total number of HTTP requests sent",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
metricHttpTotalCancelCount = promauto.NewCounter(
|
metricHttpTotalCancelCount = promauto.NewCounter(
|
||||||
prometheus.CounterOpts{
|
prometheus.CounterOpts{
|
||||||
Name: "jetkvm_timesync_http_total_cancel_count",
|
Name: "jetkvm_timesync_http_total_cancel_total",
|
||||||
Help: "The total number of HTTP requests cancelled",
|
Help: "The total number of HTTP requests cancelled",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
metricHttpSuccessCount = promauto.NewCounterVec(
|
metricHttpSuccessCount = promauto.NewCounterVec(
|
||||||
prometheus.CounterOpts{
|
prometheus.CounterOpts{
|
||||||
Name: "jetkvm_timesync_http_success_count",
|
Name: "jetkvm_timesync_http_success_total",
|
||||||
Help: "The number of successful HTTP requests",
|
Help: "The number of successful HTTP requests",
|
||||||
},
|
},
|
||||||
[]string{"url"},
|
[]string{"url"},
|
||||||
)
|
)
|
||||||
metricHttpRequestCount = promauto.NewCounterVec(
|
metricHttpRequestCount = promauto.NewCounterVec(
|
||||||
prometheus.CounterOpts{
|
prometheus.CounterOpts{
|
||||||
Name: "jetkvm_timesync_http_request_count",
|
Name: "jetkvm_timesync_http_request_total",
|
||||||
Help: "The number of HTTP requests sent to the server",
|
Help: "The number of HTTP requests sent to the server",
|
||||||
},
|
},
|
||||||
[]string{"url"},
|
[]string{"url"},
|
||||||
)
|
)
|
||||||
metricHttpCancelCount = promauto.NewCounterVec(
|
metricHttpCancelCount = promauto.NewCounterVec(
|
||||||
prometheus.CounterOpts{
|
prometheus.CounterOpts{
|
||||||
Name: "jetkvm_timesync_http_cancel_count",
|
Name: "jetkvm_timesync_http_cancel_total",
|
||||||
Help: "The number of HTTP requests cancelled",
|
Help: "The number of HTTP requests cancelled",
|
||||||
},
|
},
|
||||||
[]string{"url"},
|
[]string{"url"},
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/fsnotify/fsnotify"
|
"github.com/fsnotify/fsnotify"
|
||||||
|
@ -149,6 +150,12 @@ func (c *DHCPClient) loadLeaseFile() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
isFirstLoad := c.lease == nil
|
isFirstLoad := c.lease == nil
|
||||||
|
|
||||||
|
// Skip processing if lease hasn't changed to avoid unnecessary wake-ups.
|
||||||
|
if reflect.DeepEqual(c.lease, lease) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
c.lease = lease
|
c.lease = lease
|
||||||
|
|
||||||
if lease.IPAddress == nil {
|
if lease.IPAddress == nil {
|
||||||
|
|
|
@ -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,192 @@
|
||||||
|
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 change == nil {
|
||||||
|
c.l.Error().Str("key", key.(string)).Msg("fileChange not found")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
if change.IgnoreErrors {
|
||||||
|
c.l.Warn().Str("change", change.String()).Err(err).Msg("ignoring error")
|
||||||
|
} else {
|
||||||
|
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 (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path"
|
|
||||||
"path/filepath"
|
|
||||||
"sort"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type gadgetConfigItem struct {
|
type gadgetConfigItem struct {
|
||||||
|
@ -179,21 +175,11 @@ func (u *UsbGadget) OverrideGadgetConfig(itemKey string, itemAttr string, value
|
||||||
return nil, true
|
return nil, true
|
||||||
}
|
}
|
||||||
|
|
||||||
func mountConfigFS() error {
|
func mountConfigFS(path string) error {
|
||||||
_, err := os.Stat(gadgetPath)
|
err := exec.Command("mount", "-t", "configfs", "none", path).Run()
|
||||||
// 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 {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to mount configfs: %w", err)
|
return fmt.Errorf("failed to mount configfs: %w", err)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
return fmt.Errorf("unable to access usb gadget path: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -205,26 +191,14 @@ func (u *UsbGadget) Init() error {
|
||||||
|
|
||||||
udcs := getUdcs()
|
udcs := getUdcs()
|
||||||
if len(udcs) < 1 {
|
if len(udcs) < 1 {
|
||||||
u.log.Error().Msg("no udc found, skipping USB stack init")
|
return u.logWarn("no udc found, skipping USB stack init", nil)
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
u.udc = udcs[0]
|
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 {
|
err := u.configureUsbGadget(false)
|
||||||
u.log.Error().Err(err).Msg("failed to mount configfs, usb stack might not function properly")
|
if err != nil {
|
||||||
}
|
return u.logError("unable to initialize USB stack", err)
|
||||||
|
|
||||||
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")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -236,143 +210,22 @@ func (u *UsbGadget) UpdateGadgetConfig() error {
|
||||||
|
|
||||||
u.loadGadgetConfig()
|
u.loadGadgetConfig()
|
||||||
|
|
||||||
if err := u.writeGadgetConfig(); err != nil {
|
err := u.configureUsbGadget(true)
|
||||||
u.log.Error().Err(err).Msg("failed to update gadget")
|
if err != nil {
|
||||||
|
return u.logError("unable to update gadget config", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *UsbGadget) getOrderedConfigItems() orderedGadgetConfigItems {
|
func (u *UsbGadget) configureUsbGadget(resetUsb bool) error {
|
||||||
items := make([]gadgetConfigItemWithKey, 0)
|
return u.WithTransaction(func() error {
|
||||||
for key, item := range u.configMap {
|
u.tx.MountConfigFS()
|
||||||
items = append(items, gadgetConfigItemWithKey{key, item})
|
u.tx.CreateConfigPath()
|
||||||
|
u.tx.WriteGadgetConfig()
|
||||||
|
if resetUsb {
|
||||||
|
u.tx.RebindUsb(true)
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
sort.Slice(items, func(i, j int) bool {
|
|
||||||
return items[i].item.order < items[j].item.order
|
|
||||||
})
|
})
|
||||||
|
|
||||||
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 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,349 @@
|
||||||
|
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{
|
||||||
|
Key: "udc",
|
||||||
|
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",
|
||||||
|
DependsOn: []string{"udc"},
|
||||||
|
IgnoreErrors: ignoreUnbindError,
|
||||||
|
})
|
||||||
|
// 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")},
|
||||||
|
})
|
||||||
|
}
|
|
@ -1,8 +1,11 @@
|
||||||
package usbgadget
|
package usbgadget
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
var keyboardConfig = gadgetConfigItem{
|
var keyboardConfig = gadgetConfigItem{
|
||||||
|
@ -36,6 +39,7 @@ var keyboardReportDesc = []byte{
|
||||||
0x81, 0x03, /* INPUT (Cnst,Var,Abs) */
|
0x81, 0x03, /* INPUT (Cnst,Var,Abs) */
|
||||||
0x95, 0x05, /* REPORT_COUNT (5) */
|
0x95, 0x05, /* REPORT_COUNT (5) */
|
||||||
0x75, 0x01, /* REPORT_SIZE (1) */
|
0x75, 0x01, /* REPORT_SIZE (1) */
|
||||||
|
|
||||||
0x05, 0x08, /* USAGE_PAGE (LEDs) */
|
0x05, 0x08, /* USAGE_PAGE (LEDs) */
|
||||||
0x19, 0x01, /* USAGE_MINIMUM (Num Lock) */
|
0x19, 0x01, /* USAGE_MINIMUM (Num Lock) */
|
||||||
0x29, 0x05, /* USAGE_MAXIMUM (Kana) */
|
0x29, 0x05, /* USAGE_MAXIMUM (Kana) */
|
||||||
|
@ -54,23 +58,155 @@ var keyboardReportDesc = []byte{
|
||||||
0xc0, /* END_COLLECTION */
|
0xc0, /* END_COLLECTION */
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *UsbGadget) keyboardWriteHidFile(data []byte) error {
|
const (
|
||||||
|
hidReadBufferSize = 8
|
||||||
|
// https://www.usb.org/sites/default/files/documents/hid1_11.pdf
|
||||||
|
// https://www.usb.org/sites/default/files/hut1_2.pdf
|
||||||
|
KeyboardLedMaskNumLock = 1 << 0
|
||||||
|
KeyboardLedMaskCapsLock = 1 << 1
|
||||||
|
KeyboardLedMaskScrollLock = 1 << 2
|
||||||
|
KeyboardLedMaskCompose = 1 << 3
|
||||||
|
KeyboardLedMaskKana = 1 << 4
|
||||||
|
ValidKeyboardLedMasks = KeyboardLedMaskNumLock | KeyboardLedMaskCapsLock | KeyboardLedMaskScrollLock | KeyboardLedMaskCompose | KeyboardLedMaskKana
|
||||||
|
)
|
||||||
|
|
||||||
|
// Synchronization between LED states and CAPS LOCK, NUM LOCK, SCROLL LOCK,
|
||||||
|
// COMPOSE, and KANA events is maintained by the host and NOT the keyboard. If
|
||||||
|
// using the keyboard descriptor in Appendix B, LED states are set by sending a
|
||||||
|
// 5-bit absolute report to the keyboard via a Set_Report(Output) request.
|
||||||
|
type KeyboardState struct {
|
||||||
|
NumLock bool `json:"num_lock"`
|
||||||
|
CapsLock bool `json:"caps_lock"`
|
||||||
|
ScrollLock bool `json:"scroll_lock"`
|
||||||
|
Compose bool `json:"compose"`
|
||||||
|
Kana bool `json:"kana"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func getKeyboardState(b byte) KeyboardState {
|
||||||
|
// should we check if it's the correct usage page?
|
||||||
|
return KeyboardState{
|
||||||
|
NumLock: b&KeyboardLedMaskNumLock != 0,
|
||||||
|
CapsLock: b&KeyboardLedMaskCapsLock != 0,
|
||||||
|
ScrollLock: b&KeyboardLedMaskScrollLock != 0,
|
||||||
|
Compose: b&KeyboardLedMaskCompose != 0,
|
||||||
|
Kana: b&KeyboardLedMaskKana != 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UsbGadget) updateKeyboardState(b byte) {
|
||||||
|
u.keyboardStateLock.Lock()
|
||||||
|
defer u.keyboardStateLock.Unlock()
|
||||||
|
|
||||||
|
if b&^ValidKeyboardLedMasks != 0 {
|
||||||
|
u.log.Trace().Uint8("b", b).Msg("contains invalid bits, ignoring")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newState := getKeyboardState(b)
|
||||||
|
if reflect.DeepEqual(u.keyboardState, newState) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
u.log.Info().Interface("old", u.keyboardState).Interface("new", newState).Msg("keyboardState updated")
|
||||||
|
u.keyboardState = newState
|
||||||
|
|
||||||
|
if u.onKeyboardStateChange != nil {
|
||||||
|
(*u.onKeyboardStateChange)(newState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UsbGadget) SetOnKeyboardStateChange(f func(state KeyboardState)) {
|
||||||
|
u.onKeyboardStateChange = &f
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UsbGadget) GetKeyboardState() KeyboardState {
|
||||||
|
u.keyboardStateLock.Lock()
|
||||||
|
defer u.keyboardStateLock.Unlock()
|
||||||
|
|
||||||
|
return u.keyboardState
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UsbGadget) listenKeyboardEvents() {
|
||||||
|
var path string
|
||||||
|
if u.keyboardHidFile != nil {
|
||||||
|
path = u.keyboardHidFile.Name()
|
||||||
|
}
|
||||||
|
l := u.log.With().Str("listener", "keyboardEvents").Str("path", path).Logger()
|
||||||
|
l.Trace().Msg("starting")
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
buf := make([]byte, hidReadBufferSize)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-u.keyboardStateCtx.Done():
|
||||||
|
l.Info().Msg("context done")
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
l.Trace().Msg("reading from keyboard")
|
||||||
if u.keyboardHidFile == nil {
|
if u.keyboardHidFile == nil {
|
||||||
|
u.logWithSupression("keyboardHidFileNil", 100, &l, nil, "keyboardHidFile is nil")
|
||||||
|
// show the error every 100 times to avoid spamming the logs
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// reset the counter
|
||||||
|
u.resetLogSuppressionCounter("keyboardHidFileNil")
|
||||||
|
|
||||||
|
n, err := u.keyboardHidFile.Read(buf)
|
||||||
|
if err != nil {
|
||||||
|
u.logWithSupression("keyboardHidFileRead", 100, &l, err, "failed to read")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
u.resetLogSuppressionCounter("keyboardHidFileRead")
|
||||||
|
|
||||||
|
l.Trace().Int("n", n).Bytes("buf", buf).Msg("got data from keyboard")
|
||||||
|
if n != 1 {
|
||||||
|
l.Trace().Int("n", n).Msg("expected 1 byte, got")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
u.updateKeyboardState(buf[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UsbGadget) openKeyboardHidFile() error {
|
||||||
|
if u.keyboardHidFile != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
u.keyboardHidFile, err = os.OpenFile("/dev/hidg0", os.O_RDWR, 0666)
|
u.keyboardHidFile, err = os.OpenFile("/dev/hidg0", os.O_RDWR, 0666)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to open hidg0: %w", err)
|
return fmt.Errorf("failed to open hidg0: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if u.keyboardStateCancel != nil {
|
||||||
|
u.keyboardStateCancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
u.keyboardStateCtx, u.keyboardStateCancel = context.WithCancel(context.Background())
|
||||||
|
u.listenKeyboardEvents()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UsbGadget) OpenKeyboardHidFile() error {
|
||||||
|
return u.openKeyboardHidFile()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UsbGadget) keyboardWriteHidFile(data []byte) error {
|
||||||
|
if err := u.openKeyboardHidFile(); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := u.keyboardHidFile.Write(data)
|
_, err := u.keyboardHidFile.Write(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
u.log.Error().Err(err).Msg("failed to write to hidg0")
|
u.logWithSupression("keyboardWriteHidFile", 100, u.log, err, "failed to write to hidg0")
|
||||||
u.keyboardHidFile.Close()
|
u.keyboardHidFile.Close()
|
||||||
u.keyboardHidFile = nil
|
u.keyboardHidFile = nil
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
u.resetLogSuppressionCounter("keyboardWriteHidFile")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,7 @@ var absoluteMouseConfig = gadgetConfigItem{
|
||||||
configPath: []string{"hid.usb1"},
|
configPath: []string{"hid.usb1"},
|
||||||
attrs: gadgetAttributes{
|
attrs: gadgetAttributes{
|
||||||
"protocol": "2",
|
"protocol": "2",
|
||||||
"subclass": "1",
|
"subclass": "0",
|
||||||
"report_length": "6",
|
"report_length": "6",
|
||||||
},
|
},
|
||||||
reportDesc: absoluteMouseCombinedReportDesc,
|
reportDesc: absoluteMouseCombinedReportDesc,
|
||||||
|
@ -75,11 +75,12 @@ func (u *UsbGadget) absMouseWriteHidFile(data []byte) error {
|
||||||
|
|
||||||
_, err := u.absMouseHidFile.Write(data)
|
_, err := u.absMouseHidFile.Write(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
u.log.Error().Err(err).Msg("failed to write to hidg1")
|
u.logWithSupression("absMouseWriteHidFile", 100, u.log, err, "failed to write to hidg1")
|
||||||
u.absMouseHidFile.Close()
|
u.absMouseHidFile.Close()
|
||||||
u.absMouseHidFile = nil
|
u.absMouseHidFile = nil
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
u.resetLogSuppressionCounter("absMouseWriteHidFile")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -107,24 +108,16 @@ func (u *UsbGadget) AbsMouseWheelReport(wheelY int8) error {
|
||||||
u.absMouseLock.Lock()
|
u.absMouseLock.Lock()
|
||||||
defer u.absMouseLock.Unlock()
|
defer u.absMouseLock.Unlock()
|
||||||
|
|
||||||
// Accumulate the wheelY value
|
// Only send a report if the value is non-zero
|
||||||
u.absMouseAccumulatedWheelY += float64(wheelY) / 8.0
|
if wheelY == 0 {
|
||||||
|
|
||||||
// Only send a report if the accumulated value is significant
|
|
||||||
if abs(u.absMouseAccumulatedWheelY) < 1.0 {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
scaledWheelY := int8(u.absMouseAccumulatedWheelY)
|
|
||||||
|
|
||||||
err := u.absMouseWriteHidFile([]byte{
|
err := u.absMouseWriteHidFile([]byte{
|
||||||
2, // Report ID 2
|
2, // Report ID 2
|
||||||
byte(scaledWheelY), // Scaled Wheel Y (signed)
|
byte(wheelY), // Wheel Y (signed)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Reset the accumulator, keeping any remainder
|
|
||||||
u.absMouseAccumulatedWheelY -= float64(scaledWheelY)
|
|
||||||
|
|
||||||
u.resetUserInputTime()
|
u.resetUserInputTime()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,11 +65,12 @@ func (u *UsbGadget) relMouseWriteHidFile(data []byte) error {
|
||||||
|
|
||||||
_, err := u.relMouseHidFile.Write(data)
|
_, err := u.relMouseHidFile.Write(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
u.log.Error().Err(err).Msg("failed to write to hidg2")
|
u.logWithSupression("relMouseWriteHidFile", 100, u.log, err, "failed to write to hidg2")
|
||||||
u.relMouseHidFile.Close()
|
u.relMouseHidFile.Close()
|
||||||
u.relMouseHidFile = nil
|
u.relMouseHidFile = nil
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
u.resetLogSuppressionCounter("relMouseWriteHidFile")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
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
|
// GetUsbState returns the current state of the USB gadget
|
||||||
func (u *UsbGadget) GetUsbState() (state string) {
|
func (u *UsbGadget) GetUsbState() (state string) {
|
||||||
stateFile := path.Join("/sys/class/udc", u.udc, "state")
|
stateFile := path.Join("/sys/class/udc", u.udc, "state")
|
||||||
|
|
|
@ -3,11 +3,13 @@
|
||||||
package usbgadget
|
package usbgadget
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/jetkvm/kvm/internal/logging"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -29,6 +31,7 @@ type Config struct {
|
||||||
Manufacturer string `json:"manufacturer"`
|
Manufacturer string `json:"manufacturer"`
|
||||||
Product string `json:"product"`
|
Product string `json:"product"`
|
||||||
|
|
||||||
|
strictMode bool // when it's enabled, all warnings will be converted to errors
|
||||||
isEmpty bool
|
isEmpty bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,24 +61,42 @@ type UsbGadget struct {
|
||||||
relMouseHidFile *os.File
|
relMouseHidFile *os.File
|
||||||
relMouseLock sync.Mutex
|
relMouseLock sync.Mutex
|
||||||
|
|
||||||
|
keyboardState KeyboardState
|
||||||
|
keyboardStateLock sync.Mutex
|
||||||
|
keyboardStateCtx context.Context
|
||||||
|
keyboardStateCancel context.CancelFunc
|
||||||
|
|
||||||
enabledDevices Devices
|
enabledDevices Devices
|
||||||
|
|
||||||
|
strictMode bool // only intended for testing for now
|
||||||
|
|
||||||
absMouseAccumulatedWheelY float64
|
absMouseAccumulatedWheelY float64
|
||||||
|
|
||||||
lastUserInput time.Time
|
lastUserInput time.Time
|
||||||
|
|
||||||
|
tx *UsbGadgetTransaction
|
||||||
|
txLock sync.Mutex
|
||||||
|
|
||||||
|
onKeyboardStateChange *func(state KeyboardState)
|
||||||
|
|
||||||
log *zerolog.Logger
|
log *zerolog.Logger
|
||||||
|
|
||||||
|
logSuppressionCounter map[string]int
|
||||||
}
|
}
|
||||||
|
|
||||||
const configFSPath = "/sys/kernel/config"
|
const configFSPath = "/sys/kernel/config"
|
||||||
const gadgetPath = "/sys/kernel/config/usb_gadget"
|
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.
|
// NewUsbGadget creates a new UsbGadget.
|
||||||
func NewUsbGadget(name string, enabledDevices *Devices, config *Config, logger *zerolog.Logger) *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 {
|
if logger == nil {
|
||||||
logger = &defaultLogger
|
logger = defaultLogger
|
||||||
}
|
}
|
||||||
|
|
||||||
if enabledDevices == nil {
|
if enabledDevices == nil {
|
||||||
|
@ -86,20 +107,30 @@ func NewUsbGadget(name string, enabledDevices *Devices, config *Config, logger *
|
||||||
config = &Config{isEmpty: true}
|
config = &Config{isEmpty: true}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
keyboardCtx, keyboardCancel := context.WithCancel(context.Background())
|
||||||
|
|
||||||
g := &UsbGadget{
|
g := &UsbGadget{
|
||||||
name: name,
|
name: name,
|
||||||
kvmGadgetPath: path.Join(gadgetPath, name),
|
kvmGadgetPath: path.Join(gadgetPath, name),
|
||||||
configC1Path: path.Join(gadgetPath, name, "configs/c.1"),
|
configC1Path: path.Join(gadgetPath, name, "configs/c.1"),
|
||||||
configMap: defaultGadgetConfig,
|
configMap: configMap,
|
||||||
customConfig: *config,
|
customConfig: *config,
|
||||||
configLock: sync.Mutex{},
|
configLock: sync.Mutex{},
|
||||||
keyboardLock: sync.Mutex{},
|
keyboardLock: sync.Mutex{},
|
||||||
absMouseLock: sync.Mutex{},
|
absMouseLock: sync.Mutex{},
|
||||||
relMouseLock: sync.Mutex{},
|
relMouseLock: sync.Mutex{},
|
||||||
|
txLock: sync.Mutex{},
|
||||||
|
keyboardStateCtx: keyboardCtx,
|
||||||
|
keyboardStateCancel: keyboardCancel,
|
||||||
|
keyboardState: KeyboardState{},
|
||||||
enabledDevices: *enabledDevices,
|
enabledDevices: *enabledDevices,
|
||||||
lastUserInput: time.Now(),
|
lastUserInput: time.Now(),
|
||||||
log: logger,
|
log: logger,
|
||||||
|
|
||||||
|
strictMode: config.strictMode,
|
||||||
|
|
||||||
|
logSuppressionCounter: make(map[string]int),
|
||||||
|
|
||||||
absMouseAccumulatedWheelY: 0,
|
absMouseAccumulatedWheelY: 0,
|
||||||
}
|
}
|
||||||
if err := g.Init(); err != nil {
|
if err := g.Init(); err != nil {
|
||||||
|
|
|
@ -3,61 +3,104 @@ package usbgadget
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
)
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
// Helper function to get absolute value of float64
|
"github.com/rs/zerolog"
|
||||||
func abs(x float64) float64 {
|
)
|
||||||
if x < 0 {
|
|
||||||
return -x
|
|
||||||
}
|
|
||||||
return x
|
|
||||||
}
|
|
||||||
|
|
||||||
func joinPath(basePath string, paths []string) string {
|
func joinPath(basePath string, paths []string) string {
|
||||||
pathArr := append([]string{basePath}, paths...)
|
pathArr := append([]string{basePath}, paths...)
|
||||||
return filepath.Join(pathArr...)
|
return filepath.Join(pathArr...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func ensureSymlink(linkPath string, target string) error {
|
func hexToDecimal(hex string) (int64, error) {
|
||||||
if _, err := os.Lstat(linkPath); err == nil {
|
decimal, err := strconv.ParseInt(hex, 16, 64)
|
||||||
currentTarget, err := os.Readlink(linkPath)
|
|
||||||
if err != nil || currentTarget != target {
|
|
||||||
err = os.Remove(linkPath)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to remove existing symlink %s: %w", linkPath, err)
|
return 0, err
|
||||||
}
|
}
|
||||||
}
|
return decimal, nil
|
||||||
} else if !os.IsNotExist(err) {
|
|
||||||
return fmt.Errorf("failed to check if symlink exists: %w", 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *UsbGadget) writeIfDifferent(filePath string, content []byte, permMode os.FileMode) error {
|
func decimalToOctal(decimal int64) string {
|
||||||
if _, err := os.Stat(filePath); err == nil {
|
return fmt.Sprintf("%04o", decimal)
|
||||||
oldContent, err := os.ReadFile(filePath)
|
}
|
||||||
if err == nil {
|
|
||||||
if bytes.Equal(oldContent, content) {
|
func hexToOctal(hex string) (string, error) {
|
||||||
u.log.Trace().Str("path", filePath).Msg("skipping writing to as it already has the correct content")
|
hex = strings.ToLower(hex)
|
||||||
return nil
|
hex = strings.Replace(hex, "0x", "", 1) //remove 0x or 0X
|
||||||
}
|
|
||||||
|
decimal, err := hexToDecimal(hex)
|
||||||
if len(oldContent) == len(content)+1 &&
|
if err != nil {
|
||||||
bytes.Equal(oldContent[:len(content)], content) &&
|
return "", err
|
||||||
oldContent[len(content)] == 10 {
|
}
|
||||||
u.log.Trace().Str("path", filePath).Msg("skipping writing to as it already has the correct content")
|
|
||||||
return nil
|
// Convert the decimal integer to an octal string.
|
||||||
}
|
octal := decimalToOctal(decimal)
|
||||||
|
return octal, nil
|
||||||
u.log.Trace().Str("path", filePath).Bytes("old", oldContent).Bytes("new", content).Msg("writing to as it has different content")
|
}
|
||||||
}
|
|
||||||
}
|
func compareFileContent(oldContent []byte, newContent []byte, looserMatch bool) bool {
|
||||||
return os.WriteFile(filePath, content, permMode)
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if looserMatch {
|
||||||
|
oldContentStr := strings.TrimSpace(string(oldContent))
|
||||||
|
newContentStr := strings.TrimSpace(string(newContent))
|
||||||
|
|
||||||
|
return oldContentStr == newContentStr
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UsbGadget) logWithSupression(counterName string, every int, logger *zerolog.Logger, err error, msg string, args ...interface{}) {
|
||||||
|
if _, ok := u.logSuppressionCounter[counterName]; !ok {
|
||||||
|
u.logSuppressionCounter[counterName] = 0
|
||||||
|
} else {
|
||||||
|
u.logSuppressionCounter[counterName]++
|
||||||
|
}
|
||||||
|
|
||||||
|
l := logger.With().Int("counter", u.logSuppressionCounter[counterName]).Logger()
|
||||||
|
|
||||||
|
if u.logSuppressionCounter[counterName]%every == 0 {
|
||||||
|
if err != nil {
|
||||||
|
l.Error().Err(err).Msgf(msg, args...)
|
||||||
|
} else {
|
||||||
|
l.Error().Msgf(msg, args...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UsbGadget) resetLogSuppressionCounter(counterName string) {
|
||||||
|
if _, ok := u.logSuppressionCounter[counterName]; !ok {
|
||||||
|
u.logSuppressionCounter[counterName] = 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
package websecure
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
fixtureEd25519Certificate = `-----BEGIN CERTIFICATE-----
|
||||||
|
MIIBQDCB86ADAgECAhQdB4qB6dV0/u1lwhJofQgkmjjV1zAFBgMrZXAwLzELMAkG
|
||||||
|
A1UEBhMCREUxIDAeBgNVBAMMF2VkMjU1MTktdGVzdC5qZXRrdm0uY29tMB4XDTI1
|
||||||
|
MDUyMzEyNTkyN1oXDTI3MDQyMzEyNTkyN1owLzELMAkGA1UEBhMCREUxIDAeBgNV
|
||||||
|
BAMMF2VkMjU1MTktdGVzdC5qZXRrdm0uY29tMCowBQYDK2VwAyEA9tLyoulJn7Ev
|
||||||
|
bf8kuD1ZGdA092773pCRjFEDKpXHonyjITAfMB0GA1UdDgQWBBRkmrVMfsLY57iy
|
||||||
|
r/0POP0S4QxCADAFBgMrZXADQQBfTRvqavLHDYQiKQTgbGod+Yn+fIq2lE584+1U
|
||||||
|
C4wh9peIJDFocLBEAYTQpEMKxa4s0AIRxD+a7aCS5oz0e/0I
|
||||||
|
-----END CERTIFICATE-----`
|
||||||
|
|
||||||
|
fixtureEd25519PrivateKey = `-----BEGIN PRIVATE KEY-----
|
||||||
|
MC4CAQAwBQYDK2VwBCIEIKV08xUsLRHBfMXqZwxVRzIbViOp8G7aQGjPvoRFjujB
|
||||||
|
-----END PRIVATE KEY-----`
|
||||||
|
|
||||||
|
certStore *CertStore
|
||||||
|
certSigner *SelfSigner
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
tlsStorePath, err := os.MkdirTemp("", "jktls.*")
|
||||||
|
if err != nil {
|
||||||
|
defaultLogger.Fatal().Err(err).Msg("failed to create temp directory")
|
||||||
|
}
|
||||||
|
|
||||||
|
certStore = NewCertStore(tlsStorePath, nil)
|
||||||
|
certStore.LoadCertificates()
|
||||||
|
|
||||||
|
certSigner = NewSelfSigner(
|
||||||
|
certStore,
|
||||||
|
nil,
|
||||||
|
"ci.jetkvm.com",
|
||||||
|
"JetKVM",
|
||||||
|
"JetKVM",
|
||||||
|
"JetKVM",
|
||||||
|
)
|
||||||
|
|
||||||
|
m.Run()
|
||||||
|
|
||||||
|
os.RemoveAll(tlsStorePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSaveEd25519Certificate(t *testing.T) {
|
||||||
|
err, _ := certStore.ValidateAndSaveCertificate("ed25519-test.jetkvm.com", fixtureEd25519Certificate, fixtureEd25519PrivateKey, true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to save certificate: %v", err)
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,6 +2,7 @@ package websecure
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/ecdsa"
|
"crypto/ecdsa"
|
||||||
|
"crypto/ed25519"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
@ -37,11 +38,15 @@ func keyToFile(cert *tls.Certificate, filename string) error {
|
||||||
if e != nil {
|
if e != nil {
|
||||||
return fmt.Errorf("failed to marshal EC private key: %v", e)
|
return fmt.Errorf("failed to marshal EC private key: %v", e)
|
||||||
}
|
}
|
||||||
|
|
||||||
keyBlock = pem.Block{
|
keyBlock = pem.Block{
|
||||||
Type: "EC PRIVATE KEY",
|
Type: "EC PRIVATE KEY",
|
||||||
Bytes: b,
|
Bytes: b,
|
||||||
}
|
}
|
||||||
|
case ed25519.PrivateKey:
|
||||||
|
keyBlock = pem.Block{
|
||||||
|
Type: "ED25519 PRIVATE KEY",
|
||||||
|
Bytes: k,
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unknown private key type: %T", k)
|
return fmt.Errorf("unknown private key type: %T", k)
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,9 +15,7 @@ func rpcGetJigglerState() bool {
|
||||||
return jigglerEnabled
|
return jigglerEnabled
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func initJiggler() {
|
||||||
ensureConfigLoaded()
|
|
||||||
|
|
||||||
go runJiggler()
|
go runJiggler()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
75
jsonrpc.go
75
jsonrpc.go
|
@ -266,9 +266,14 @@ func rpcSetDevChannelState(enabled bool) error {
|
||||||
func rpcGetUpdateStatus() (*UpdateStatus, error) {
|
func rpcGetUpdateStatus() (*UpdateStatus, error) {
|
||||||
includePreRelease := config.IncludePreRelease
|
includePreRelease := config.IncludePreRelease
|
||||||
updateStatus, err := GetUpdateStatus(context.Background(), GetDeviceID(), includePreRelease)
|
updateStatus, err := GetUpdateStatus(context.Background(), GetDeviceID(), includePreRelease)
|
||||||
|
// to ensure backwards compatibility,
|
||||||
|
// if there's an error, we won't return an error, but we will set the error field
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if updateStatus == nil {
|
||||||
return nil, fmt.Errorf("error checking for updates: %w", err)
|
return nil, fmt.Errorf("error checking for updates: %w", err)
|
||||||
}
|
}
|
||||||
|
updateStatus.Error = err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
return updateStatus, nil
|
return updateStatus, nil
|
||||||
}
|
}
|
||||||
|
@ -461,7 +466,31 @@ func rpcSetTLSState(state TLSState) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func callRPCHandler(handler RPCHandler, params map[string]interface{}) (interface{}, error) {
|
type RPCHandler struct {
|
||||||
|
Func interface{}
|
||||||
|
Params []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// call the handler but recover from a panic to ensure our RPC thread doesn't collapse on malformed calls
|
||||||
|
func callRPCHandler(handler RPCHandler, params map[string]interface{}) (result interface{}, err error) {
|
||||||
|
// Use defer to recover from a panic
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
// Convert the panic to an error
|
||||||
|
if e, ok := r.(error); ok {
|
||||||
|
err = e
|
||||||
|
} else {
|
||||||
|
err = fmt.Errorf("panic occurred: %v", r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Call the handler
|
||||||
|
result, err = riskyCallRPCHandler(handler, params)
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func riskyCallRPCHandler(handler RPCHandler, params map[string]interface{}) (interface{}, error) {
|
||||||
handlerValue := reflect.ValueOf(handler.Func)
|
handlerValue := reflect.ValueOf(handler.Func)
|
||||||
handlerType := handlerValue.Type()
|
handlerType := handlerValue.Type()
|
||||||
|
|
||||||
|
@ -558,11 +587,6 @@ func callRPCHandler(handler RPCHandler, params map[string]interface{}) (interfac
|
||||||
return nil, errors.New("unexpected return values from handler")
|
return nil, errors.New("unexpected return values from handler")
|
||||||
}
|
}
|
||||||
|
|
||||||
type RPCHandler struct {
|
|
||||||
Func interface{}
|
|
||||||
Params []string
|
|
||||||
}
|
|
||||||
|
|
||||||
func rpcSetMassStorageMode(mode string) (string, error) {
|
func rpcSetMassStorageMode(mode string) (string, error) {
|
||||||
logger.Info().Str("mode", mode).Msg("Setting mass storage mode")
|
logger.Info().Str("mode", mode).Msg("Setting mass storage mode")
|
||||||
var cdrom bool
|
var cdrom bool
|
||||||
|
@ -877,14 +901,15 @@ func rpcSetCloudUrl(apiUrl string, appUrl string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var currentScrollSensitivity string = "default"
|
func rpcGetKeyboardLayout() (string, error) {
|
||||||
|
return config.KeyboardLayout, nil
|
||||||
func rpcGetScrollSensitivity() (string, error) {
|
|
||||||
return currentScrollSensitivity, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func rpcSetScrollSensitivity(sensitivity string) error {
|
func rpcSetKeyboardLayout(layout string) error {
|
||||||
currentScrollSensitivity = sensitivity
|
config.KeyboardLayout = layout
|
||||||
|
if err := SaveConfig(); err != nil {
|
||||||
|
return fmt.Errorf("failed to save config: %w", err)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -981,6 +1006,25 @@ func setKeyboardMacros(params KeyboardMacrosParams) (interface{}, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func rpcGetLocalLoopbackOnly() (bool, error) {
|
||||||
|
return config.LocalLoopbackOnly, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func rpcSetLocalLoopbackOnly(enabled bool) error {
|
||||||
|
// Check if the setting is actually changing
|
||||||
|
if config.LocalLoopbackOnly == enabled {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the setting
|
||||||
|
config.LocalLoopbackOnly = enabled
|
||||||
|
if err := SaveConfig(); err != nil {
|
||||||
|
return fmt.Errorf("failed to save config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
var rpcHandlers = map[string]RPCHandler{
|
var rpcHandlers = map[string]RPCHandler{
|
||||||
"ping": {Func: rpcPing},
|
"ping": {Func: rpcPing},
|
||||||
"reboot": {Func: rpcReboot, Params: []string{"force"}},
|
"reboot": {Func: rpcReboot, Params: []string{"force"}},
|
||||||
|
@ -992,6 +1036,7 @@ var rpcHandlers = map[string]RPCHandler{
|
||||||
"setNetworkSettings": {Func: rpcSetNetworkSettings, Params: []string{"settings"}},
|
"setNetworkSettings": {Func: rpcSetNetworkSettings, Params: []string{"settings"}},
|
||||||
"renewDHCPLease": {Func: rpcRenewDHCPLease},
|
"renewDHCPLease": {Func: rpcRenewDHCPLease},
|
||||||
"keyboardReport": {Func: rpcKeyboardReport, Params: []string{"modifier", "keys"}},
|
"keyboardReport": {Func: rpcKeyboardReport, Params: []string{"modifier", "keys"}},
|
||||||
|
"getKeyboardLedState": {Func: rpcGetKeyboardLedState},
|
||||||
"absMouseReport": {Func: rpcAbsMouseReport, Params: []string{"x", "y", "buttons"}},
|
"absMouseReport": {Func: rpcAbsMouseReport, Params: []string{"x", "y", "buttons"}},
|
||||||
"relMouseReport": {Func: rpcRelMouseReport, Params: []string{"dx", "dy", "buttons"}},
|
"relMouseReport": {Func: rpcRelMouseReport, Params: []string{"dx", "dy", "buttons"}},
|
||||||
"wheelReport": {Func: rpcWheelReport, Params: []string{"wheelY"}},
|
"wheelReport": {Func: rpcWheelReport, Params: []string{"wheelY"}},
|
||||||
|
@ -1053,8 +1098,10 @@ var rpcHandlers = map[string]RPCHandler{
|
||||||
"setUsbDevices": {Func: rpcSetUsbDevices, Params: []string{"devices"}},
|
"setUsbDevices": {Func: rpcSetUsbDevices, Params: []string{"devices"}},
|
||||||
"setUsbDeviceState": {Func: rpcSetUsbDeviceState, Params: []string{"device", "enabled"}},
|
"setUsbDeviceState": {Func: rpcSetUsbDeviceState, Params: []string{"device", "enabled"}},
|
||||||
"setCloudUrl": {Func: rpcSetCloudUrl, Params: []string{"apiUrl", "appUrl"}},
|
"setCloudUrl": {Func: rpcSetCloudUrl, Params: []string{"apiUrl", "appUrl"}},
|
||||||
"getScrollSensitivity": {Func: rpcGetScrollSensitivity},
|
"getKeyboardLayout": {Func: rpcGetKeyboardLayout},
|
||||||
"setScrollSensitivity": {Func: rpcSetScrollSensitivity, Params: []string{"sensitivity"}},
|
"setKeyboardLayout": {Func: rpcSetKeyboardLayout, Params: []string{"layout"}},
|
||||||
"getKeyboardMacros": {Func: getKeyboardMacros},
|
"getKeyboardMacros": {Func: getKeyboardMacros},
|
||||||
"setKeyboardMacros": {Func: setKeyboardMacros, Params: []string{"params"}},
|
"setKeyboardMacros": {Func: setKeyboardMacros, Params: []string{"params"}},
|
||||||
|
"getLocalLoopbackOnly": {Func: rpcGetLocalLoopbackOnly},
|
||||||
|
"setLocalLoopbackOnly": {Func: rpcSetLocalLoopbackOnly, Params: []string{"enabled"}},
|
||||||
}
|
}
|
||||||
|
|
7
main.go
7
main.go
|
@ -75,8 +75,9 @@ func Main() {
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
// initialize usb gadget
|
||||||
initUsbGadget()
|
initUsbGadget()
|
||||||
StartNtpAudioServer(handleAudioClient)
|
StartRtpAudioServer(handleAudioClient)
|
||||||
|
|
||||||
if err := setInitialVirtualMediaState(); err != nil {
|
if err := setInitialVirtualMediaState(); err != nil {
|
||||||
logger.Warn().Err(err).Msg("failed to set initial virtual media state")
|
logger.Warn().Err(err).Msg("failed to set initial virtual media state")
|
||||||
|
@ -85,6 +86,10 @@ func Main() {
|
||||||
if err := initImagesFolder(); err != nil {
|
if err := initImagesFolder(); err != nil {
|
||||||
logger.Warn().Err(err).Msg("failed to init images folder")
|
logger.Warn().Err(err).Msg("failed to init images folder")
|
||||||
}
|
}
|
||||||
|
initJiggler()
|
||||||
|
|
||||||
|
// initialize display
|
||||||
|
initDisplay()
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
time.Sleep(15 * time.Minute)
|
time.Sleep(15 * time.Minute)
|
||||||
|
|
85
native.go
85
native.go
|
@ -8,6 +8,7 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -42,6 +43,11 @@ var ongoingRequests = make(map[int32]chan *CtrlResponse)
|
||||||
|
|
||||||
var lock = &sync.Mutex{}
|
var lock = &sync.Mutex{}
|
||||||
|
|
||||||
|
var (
|
||||||
|
nativeCmd *exec.Cmd
|
||||||
|
nativeCmdLock = &sync.Mutex{}
|
||||||
|
)
|
||||||
|
|
||||||
func CallCtrlAction(action string, params map[string]interface{}) (*CtrlResponse, error) {
|
func CallCtrlAction(action string, params map[string]interface{}) (*CtrlResponse, error) {
|
||||||
lock.Lock()
|
lock.Lock()
|
||||||
defer lock.Unlock()
|
defer lock.Unlock()
|
||||||
|
@ -130,16 +136,26 @@ func StartNativeSocketServer(socketPath string, handleClient func(net.Conn), isC
|
||||||
scopedLogger.Info().Msg("server listening")
|
scopedLogger.Info().Msg("server listening")
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
|
for {
|
||||||
conn, err := listener.Accept()
|
conn, err := listener.Accept()
|
||||||
listener.Close()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
scopedLogger.Warn().Err(err).Msg("failed to accept socket")
|
scopedLogger.Warn().Err(err).Msg("failed to accept socket")
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
if isCtrl {
|
if isCtrl {
|
||||||
|
// check if the channel is closed
|
||||||
|
select {
|
||||||
|
case <-ctrlClientConnected:
|
||||||
|
scopedLogger.Debug().Msg("ctrl client reconnected")
|
||||||
|
default:
|
||||||
close(ctrlClientConnected)
|
close(ctrlClientConnected)
|
||||||
scopedLogger.Debug().Msg("first native ctrl socket client connected")
|
scopedLogger.Debug().Msg("first native ctrl socket client connected")
|
||||||
}
|
}
|
||||||
handleClient(conn)
|
}
|
||||||
|
|
||||||
|
go handleClient(conn)
|
||||||
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
return listener
|
return listener
|
||||||
|
@ -274,6 +290,51 @@ func handleAudioClient(conn net.Conn) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func startNativeBinaryWithLock(binaryPath string) (*exec.Cmd, error) {
|
||||||
|
nativeCmdLock.Lock()
|
||||||
|
defer nativeCmdLock.Unlock()
|
||||||
|
|
||||||
|
cmd, err := startNativeBinary(binaryPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
nativeCmd = cmd
|
||||||
|
return cmd, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func restartNativeBinary(binaryPath string) error {
|
||||||
|
time.Sleep(10 * time.Second)
|
||||||
|
// restart the binary
|
||||||
|
nativeLogger.Info().Msg("restarting jetkvm_native binary")
|
||||||
|
cmd, err := startNativeBinary(binaryPath)
|
||||||
|
if err != nil {
|
||||||
|
nativeLogger.Warn().Err(err).Msg("failed to restart binary")
|
||||||
|
}
|
||||||
|
nativeCmd = cmd
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func superviseNativeBinary(binaryPath string) error {
|
||||||
|
nativeCmdLock.Lock()
|
||||||
|
defer nativeCmdLock.Unlock()
|
||||||
|
|
||||||
|
if nativeCmd == nil || nativeCmd.Process == nil {
|
||||||
|
return restartNativeBinary(binaryPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := nativeCmd.Wait()
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
nativeLogger.Info().Err(err).Msg("jetkvm_native binary exited with no error")
|
||||||
|
} else if exiterr, ok := err.(*exec.ExitError); ok {
|
||||||
|
nativeLogger.Warn().Int("exit_code", exiterr.ExitCode()).Msg("jetkvm_native binary exited with error")
|
||||||
|
} else {
|
||||||
|
nativeLogger.Warn().Err(err).Msg("jetkvm_native binary exited with unknown error")
|
||||||
|
}
|
||||||
|
|
||||||
|
return restartNativeBinary(binaryPath)
|
||||||
|
}
|
||||||
|
|
||||||
func ExtractAndRunNativeBin() error {
|
func ExtractAndRunNativeBin() error {
|
||||||
binaryPath := "/userdata/jetkvm/bin/jetkvm_native"
|
binaryPath := "/userdata/jetkvm/bin/jetkvm_native"
|
||||||
if err := ensureBinaryUpdated(binaryPath); err != nil {
|
if err := ensureBinaryUpdated(binaryPath); err != nil {
|
||||||
|
@ -285,12 +346,28 @@ func ExtractAndRunNativeBin() error {
|
||||||
return fmt.Errorf("failed to make binary executable: %w", err)
|
return fmt.Errorf("failed to make binary executable: %w", err)
|
||||||
}
|
}
|
||||||
// Run the binary in the background
|
// Run the binary in the background
|
||||||
cmd, err := startNativeBinary(binaryPath)
|
cmd, err := startNativeBinaryWithLock(binaryPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to start binary: %w", err)
|
return fmt.Errorf("failed to start binary: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: add auto restart
|
// check if the binary is still running every 10 seconds
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-appCtx.Done():
|
||||||
|
nativeLogger.Info().Msg("stopping native binary supervisor")
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
err := superviseNativeBinary(binaryPath)
|
||||||
|
if err != nil {
|
||||||
|
nativeLogger.Warn().Err(err).Msg("failed to supervise native binary")
|
||||||
|
time.Sleep(1 * time.Second) // Add a short delay to prevent rapid successive calls
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
<-appCtx.Done()
|
<-appCtx.Done()
|
||||||
nativeLogger.Info().Int("pid", cmd.Process.Pid).Msg("killing process")
|
nativeLogger.Info().Int("pid", cmd.Process.Pid).Msg("killing process")
|
||||||
|
|
|
@ -21,6 +21,7 @@ func networkStateChanged() {
|
||||||
|
|
||||||
// always restart mDNS when the network state changes
|
// always restart mDNS when the network state changes
|
||||||
if mDNS != nil {
|
if mDNS != nil {
|
||||||
|
_ = mDNS.SetListenOptions(config.NetworkConfig.GetMDNSMode())
|
||||||
_ = mDNS.SetLocalNames([]string{
|
_ = mDNS.SetLocalNames([]string{
|
||||||
networkState.GetHostname(),
|
networkState.GetHostname(),
|
||||||
networkState.GetFQDN(),
|
networkState.GetFQDN(),
|
||||||
|
@ -54,14 +55,6 @@ func initNetwork() error {
|
||||||
OnConfigChange: func(networkConfig *network.NetworkConfig) {
|
OnConfigChange: func(networkConfig *network.NetworkConfig) {
|
||||||
config.NetworkConfig = networkConfig
|
config.NetworkConfig = networkConfig
|
||||||
networkStateChanged()
|
networkStateChanged()
|
||||||
|
|
||||||
if mDNS != nil {
|
|
||||||
_ = mDNS.SetListenOptions(networkConfig.GetMDNSMode())
|
|
||||||
_ = mDNS.SetLocalNames([]string{
|
|
||||||
networkState.GetHostname(),
|
|
||||||
networkState.GetFQDN(),
|
|
||||||
}, true)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
40
ota.go
40
ota.go
|
@ -41,6 +41,9 @@ type UpdateStatus struct {
|
||||||
Remote *UpdateMetadata `json:"remote"`
|
Remote *UpdateMetadata `json:"remote"`
|
||||||
SystemUpdateAvailable bool `json:"systemUpdateAvailable"`
|
SystemUpdateAvailable bool `json:"systemUpdateAvailable"`
|
||||||
AppUpdateAvailable bool `json:"appUpdateAvailable"`
|
AppUpdateAvailable bool `json:"appUpdateAvailable"`
|
||||||
|
|
||||||
|
// for backwards compatibility
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
const UpdateMetadataUrl = "https://api.jetkvm.com/releases"
|
const UpdateMetadataUrl = "https://api.jetkvm.com/releases"
|
||||||
|
@ -489,52 +492,47 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetUpdateStatus(ctx context.Context, deviceId string, includePreRelease bool) (*UpdateStatus, error) {
|
func GetUpdateStatus(ctx context.Context, deviceId string, includePreRelease bool) (*UpdateStatus, error) {
|
||||||
|
updateStatus := &UpdateStatus{}
|
||||||
|
|
||||||
// Get local versions
|
// Get local versions
|
||||||
systemVersionLocal, appVersionLocal, err := GetLocalVersion()
|
systemVersionLocal, appVersionLocal, err := GetLocalVersion()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error getting local version: %w", err)
|
return updateStatus, fmt.Errorf("error getting local version: %w", err)
|
||||||
|
}
|
||||||
|
updateStatus.Local = &LocalMetadata{
|
||||||
|
AppVersion: appVersionLocal.String(),
|
||||||
|
SystemVersion: systemVersionLocal.String(),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get remote metadata
|
// Get remote metadata
|
||||||
remoteMetadata, err := fetchUpdateMetadata(ctx, deviceId, includePreRelease)
|
remoteMetadata, err := fetchUpdateMetadata(ctx, deviceId, includePreRelease)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error checking for updates: %w", err)
|
return updateStatus, fmt.Errorf("error checking for updates: %w", err)
|
||||||
}
|
|
||||||
|
|
||||||
// Build local UpdateMetadata
|
|
||||||
localMetadata := &LocalMetadata{
|
|
||||||
AppVersion: appVersionLocal.String(),
|
|
||||||
SystemVersion: systemVersionLocal.String(),
|
|
||||||
}
|
}
|
||||||
|
updateStatus.Remote = remoteMetadata
|
||||||
|
|
||||||
|
// Get remote versions
|
||||||
systemVersionRemote, err := semver.NewVersion(remoteMetadata.SystemVersion)
|
systemVersionRemote, err := semver.NewVersion(remoteMetadata.SystemVersion)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error parsing remote system version: %w", err)
|
return updateStatus, fmt.Errorf("error parsing remote system version: %w", err)
|
||||||
}
|
}
|
||||||
appVersionRemote, err := semver.NewVersion(remoteMetadata.AppVersion)
|
appVersionRemote, err := semver.NewVersion(remoteMetadata.AppVersion)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error parsing remote app version: %w, %s", err, remoteMetadata.AppVersion)
|
return updateStatus, fmt.Errorf("error parsing remote app version: %w, %s", err, remoteMetadata.AppVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
systemUpdateAvailable := systemVersionRemote.GreaterThan(systemVersionLocal)
|
updateStatus.SystemUpdateAvailable = systemVersionRemote.GreaterThan(systemVersionLocal)
|
||||||
appUpdateAvailable := appVersionRemote.GreaterThan(appVersionLocal)
|
updateStatus.AppUpdateAvailable = appVersionRemote.GreaterThan(appVersionLocal)
|
||||||
|
|
||||||
// Handle pre-release updates
|
// Handle pre-release updates
|
||||||
isRemoteSystemPreRelease := systemVersionRemote.Prerelease() != ""
|
isRemoteSystemPreRelease := systemVersionRemote.Prerelease() != ""
|
||||||
isRemoteAppPreRelease := appVersionRemote.Prerelease() != ""
|
isRemoteAppPreRelease := appVersionRemote.Prerelease() != ""
|
||||||
|
|
||||||
if isRemoteSystemPreRelease && !includePreRelease {
|
if isRemoteSystemPreRelease && !includePreRelease {
|
||||||
systemUpdateAvailable = false
|
updateStatus.SystemUpdateAvailable = false
|
||||||
}
|
}
|
||||||
if isRemoteAppPreRelease && !includePreRelease {
|
if isRemoteAppPreRelease && !includePreRelease {
|
||||||
appUpdateAvailable = false
|
updateStatus.AppUpdateAvailable = false
|
||||||
}
|
|
||||||
|
|
||||||
updateStatus := &UpdateStatus{
|
|
||||||
Local: localMetadata,
|
|
||||||
Remote: remoteMetadata,
|
|
||||||
SystemUpdateAvailable: systemUpdateAvailable,
|
|
||||||
AppUpdateAvailable: appUpdateAvailable,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return updateStatus, nil
|
return updateStatus, nil
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
#!/bin/sh
|
||||||
|
JSON_OUTPUT=false
|
||||||
|
GET_COMMANDS=false
|
||||||
|
if [ "$1" = "-json" ]; then
|
||||||
|
JSON_OUTPUT=true
|
||||||
|
shift
|
||||||
|
fi
|
||||||
|
ADDITIONAL_ARGS=$@
|
||||||
|
EXIT_CODE=0
|
||||||
|
|
||||||
|
runTest() {
|
||||||
|
PKG_ARGS=""
|
||||||
|
if [ "$2" != "" ]; then
|
||||||
|
PKG_ARGS="-p $2"
|
||||||
|
fi
|
||||||
|
if [ "$JSON_OUTPUT" = true ]; then
|
||||||
|
./test2json $PKG_ARGS -t $1 -test.v $ADDITIONAL_ARGS | tee $1.result.json
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
EXIT_CODE=1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
$@
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
EXIT_CODE=1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
function exit_with_code() {
|
||||||
|
if [ $EXIT_CODE -ne 0 ]; then
|
||||||
|
printf "\e[0;31m❌ Test failed\e[0m\n"
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit $EXIT_CODE
|
||||||
|
}
|
||||||
|
|
||||||
|
trap exit_with_code EXIT
|
|
@ -1,6 +1,7 @@
|
||||||
package kvm
|
package kvm
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
@ -55,19 +56,24 @@ func handleTerminalChannel(d *webrtc.DataChannel) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if msg.IsString {
|
if msg.IsString {
|
||||||
|
maybeJson := bytes.TrimSpace(msg.Data)
|
||||||
|
// Cheap check to see if this resembles JSON
|
||||||
|
if len(maybeJson) > 1 && maybeJson[0] == '{' && maybeJson[len(maybeJson)-1] == '}' {
|
||||||
var size TerminalSize
|
var size TerminalSize
|
||||||
err := json.Unmarshal([]byte(msg.Data), &size)
|
err := json.Unmarshal(maybeJson, &size)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
err = pty.Setsize(ptmx, &pty.Winsize{
|
err = pty.Setsize(ptmx, &pty.Winsize{
|
||||||
Rows: uint16(size.Rows),
|
Rows: uint16(size.Rows),
|
||||||
Cols: uint16(size.Cols),
|
Cols: uint16(size.Cols),
|
||||||
})
|
})
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
scopedLogger.Info().Int("rows", size.Rows).Int("cols", size.Cols).Msg("Set terminal size")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
scopedLogger.Warn().Err(err).Msg("Failed to parse terminal size")
|
scopedLogger.Warn().Err(err).Msg("Failed to parse terminal size")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
_, err := ptmx.Write(msg.Data)
|
_, err := ptmx.Write(msg.Data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
scopedLogger.Warn().Err(err).Msg("Failed to write to pty")
|
scopedLogger.Warn().Err(err).Msg("Failed to write to pty")
|
||||||
|
|
|
@ -1,66 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
root: true,
|
|
||||||
env: { browser: true, es2020: true },
|
|
||||||
extends: [
|
|
||||||
"eslint:recommended",
|
|
||||||
"plugin:@typescript-eslint/recommended",
|
|
||||||
"plugin:@typescript-eslint/stylistic",
|
|
||||||
"plugin:react-hooks/recommended",
|
|
||||||
"plugin:react/recommended",
|
|
||||||
"plugin:react/jsx-runtime",
|
|
||||||
"plugin:import/recommended",
|
|
||||||
"prettier",
|
|
||||||
],
|
|
||||||
ignorePatterns: ["dist", ".eslintrc.cjs", "tailwind.config.js", "postcss.config.js"],
|
|
||||||
parser: "@typescript-eslint/parser",
|
|
||||||
plugins: ["react-refresh"],
|
|
||||||
parserOptions: {
|
|
||||||
ecmaVersion: "latest",
|
|
||||||
sourceType: "module",
|
|
||||||
project: ["./tsconfig.json", "./tsconfig.node.json"],
|
|
||||||
tsconfigRootDir: __dirname,
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
|
|
||||||
"import/order": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @description
|
|
||||||
*
|
|
||||||
* This keeps imports separate from one another, ensuring that imports are separated
|
|
||||||
* by their relative groups. As you move through the groups, imports become closer
|
|
||||||
* to the current file.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```
|
|
||||||
* import fs from 'fs';
|
|
||||||
*
|
|
||||||
* import package from 'npm-package';
|
|
||||||
*
|
|
||||||
* import xyz from '~/project-file';
|
|
||||||
*
|
|
||||||
* import index from '../';
|
|
||||||
*
|
|
||||||
* import sibling from './foo';
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
groups: ["builtin", "external", "internal", "parent", "sibling"],
|
|
||||||
"newlines-between": "always",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
settings: {
|
|
||||||
"import/resolver": {
|
|
||||||
alias: {
|
|
||||||
map: [
|
|
||||||
["@components", "./src/components"],
|
|
||||||
["@routes", "./src/routes"],
|
|
||||||
["@assets", "./src/assets"],
|
|
||||||
["@", "./src"],
|
|
||||||
],
|
|
||||||
extensions: [".ts", ".tsx", ".js", ".jsx", ".json"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
|
@ -6,6 +6,7 @@
|
||||||
"arrowParens": "avoid",
|
"arrowParens": "avoid",
|
||||||
"singleQuote": false,
|
"singleQuote": false,
|
||||||
"plugins": ["prettier-plugin-tailwindcss"],
|
"plugins": ["prettier-plugin-tailwindcss"],
|
||||||
"tailwindFunctions": ["clsx"],
|
"tailwindFunctions": ["clsx", "cx"],
|
||||||
"printWidth": 90
|
"printWidth": 90,
|
||||||
|
"tailwindStylesheet": "./src/index.css"
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,93 @@
|
||||||
|
const {
|
||||||
|
defineConfig,
|
||||||
|
globalIgnores,
|
||||||
|
} = require("eslint/config");
|
||||||
|
|
||||||
|
const globals = require("globals");
|
||||||
|
|
||||||
|
const {
|
||||||
|
fixupConfigRules,
|
||||||
|
} = require("@eslint/compat");
|
||||||
|
|
||||||
|
const tsParser = require("@typescript-eslint/parser");
|
||||||
|
const reactRefresh = require("eslint-plugin-react-refresh");
|
||||||
|
const js = require("@eslint/js");
|
||||||
|
|
||||||
|
const {
|
||||||
|
FlatCompat,
|
||||||
|
} = require("@eslint/eslintrc");
|
||||||
|
|
||||||
|
const compat = new FlatCompat({
|
||||||
|
baseDirectory: __dirname,
|
||||||
|
recommendedConfig: js.configs.recommended,
|
||||||
|
allConfig: js.configs.all
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = defineConfig([{
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
...globals.browser,
|
||||||
|
},
|
||||||
|
|
||||||
|
parser: tsParser,
|
||||||
|
ecmaVersion: "latest",
|
||||||
|
sourceType: "module",
|
||||||
|
|
||||||
|
parserOptions: {
|
||||||
|
project: ["./tsconfig.json", "./tsconfig.node.json"],
|
||||||
|
tsconfigRootDir: __dirname,
|
||||||
|
ecmaFeatures: {
|
||||||
|
jsx: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
extends: fixupConfigRules(compat.extends(
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended",
|
||||||
|
"plugin:@typescript-eslint/stylistic",
|
||||||
|
"plugin:react-hooks/recommended",
|
||||||
|
"plugin:react/recommended",
|
||||||
|
"plugin:react/jsx-runtime",
|
||||||
|
"plugin:import/recommended",
|
||||||
|
"prettier",
|
||||||
|
)),
|
||||||
|
|
||||||
|
plugins: {
|
||||||
|
"react-refresh": reactRefresh,
|
||||||
|
},
|
||||||
|
|
||||||
|
rules: {
|
||||||
|
"react-refresh/only-export-components": ["warn", {
|
||||||
|
allowConstantExport: true,
|
||||||
|
}],
|
||||||
|
|
||||||
|
"import/order": ["error", {
|
||||||
|
groups: ["builtin", "external", "internal", "parent", "sibling"],
|
||||||
|
"newlines-between": "always",
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
|
||||||
|
settings: {
|
||||||
|
"react": {
|
||||||
|
"version": "detect"
|
||||||
|
},
|
||||||
|
"import/resolver": {
|
||||||
|
alias: {
|
||||||
|
map: [
|
||||||
|
["@components", "./src/components"],
|
||||||
|
["@routes", "./src/routes"],
|
||||||
|
["@assets", "./src/assets"],
|
||||||
|
["@", "./src"],
|
||||||
|
],
|
||||||
|
|
||||||
|
extensions: [".ts", ".tsx", ".js", ".jsx", ".json"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, globalIgnores([
|
||||||
|
"**/dist",
|
||||||
|
"**/.eslintrc.cjs",
|
||||||
|
"**/tailwind.config.js",
|
||||||
|
"**/postcss.config.js",
|
||||||
|
])]);
|
File diff suppressed because it is too large
Load Diff
|
@ -4,7 +4,7 @@
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "21.1.0"
|
"node": "22.15.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "./dev_device.sh",
|
"dev": "./dev_device.sh",
|
||||||
|
@ -19,21 +19,21 @@
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@headlessui/react": "^2.2.2",
|
"@headlessui/react": "^2.2.3",
|
||||||
"@headlessui/tailwindcss": "^0.2.2",
|
"@headlessui/tailwindcss": "^0.2.2",
|
||||||
"@heroicons/react": "^2.2.0",
|
"@heroicons/react": "^2.2.0",
|
||||||
"@vitejs/plugin-basic-ssl": "^1.2.0",
|
"@vitejs/plugin-basic-ssl": "^2.0.0",
|
||||||
"@xterm/addon-clipboard": "^0.1.0",
|
"@xterm/addon-clipboard": "^0.1.0",
|
||||||
"@xterm/addon-fit": "^0.10.0",
|
"@xterm/addon-fit": "^0.10.0",
|
||||||
"@xterm/addon-unicode11": "^0.8.0",
|
"@xterm/addon-unicode11": "^0.8.0",
|
||||||
"@xterm/addon-web-links": "^0.11.0",
|
"@xterm/addon-web-links": "^0.11.0",
|
||||||
"@xterm/addon-webgl": "^0.18.0",
|
"@xterm/addon-webgl": "^0.18.0",
|
||||||
"@xterm/xterm": "^5.5.0",
|
"@xterm/xterm": "^5.5.0",
|
||||||
"cva": "^1.0.0-beta.1",
|
"cva": "^1.0.0-beta.3",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"eslint-import-resolver-alias": "^1.1.2",
|
"eslint-import-resolver-alias": "^1.1.2",
|
||||||
"focus-trap-react": "^10.2.3",
|
"focus-trap-react": "^11.0.3",
|
||||||
"framer-motion": "^11.15.0",
|
"framer-motion": "^12.11.4",
|
||||||
"lodash.throttle": "^4.1.1",
|
"lodash.throttle": "^4.1.1",
|
||||||
"mini-svg-data-uri": "^1.4.4",
|
"mini-svg-data-uri": "^1.4.4",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
|
@ -42,24 +42,29 @@
|
||||||
"react-hot-toast": "^2.5.2",
|
"react-hot-toast": "^2.5.2",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
"react-router-dom": "^6.22.3",
|
"react-router-dom": "^6.22.3",
|
||||||
"react-simple-keyboard": "^3.8.71",
|
"react-simple-keyboard": "^3.8.72",
|
||||||
"react-use-websocket": "^4.13.0",
|
"react-use-websocket": "^4.13.0",
|
||||||
"react-xtermjs": "^1.0.10",
|
"react-xtermjs": "^1.0.10",
|
||||||
"recharts": "^2.15.3",
|
"recharts": "^2.15.3",
|
||||||
"tailwind-merge": "^2.5.5",
|
"tailwind-merge": "^3.3.0",
|
||||||
"usehooks-ts": "^3.1.1",
|
"usehooks-ts": "^3.1.1",
|
||||||
"validator": "^13.15.0",
|
"validator": "^13.15.0",
|
||||||
"zustand": "^4.5.2"
|
"zustand": "^4.5.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@eslint/compat": "^1.2.9",
|
||||||
|
"@eslint/eslintrc": "^3.3.1",
|
||||||
|
"@eslint/js": "^9.26.0",
|
||||||
"@tailwindcss/forms": "^0.5.10",
|
"@tailwindcss/forms": "^0.5.10",
|
||||||
|
"@tailwindcss/postcss": "^4.1.7",
|
||||||
"@tailwindcss/typography": "^0.5.16",
|
"@tailwindcss/typography": "^0.5.16",
|
||||||
"@types/react": "^19.1.3",
|
"@tailwindcss/vite": "^4.1.7",
|
||||||
"@types/react-dom": "^19.1.3",
|
"@types/react": "^19.1.4",
|
||||||
|
"@types/react-dom": "^19.1.5",
|
||||||
"@types/semver": "^7.7.0",
|
"@types/semver": "^7.7.0",
|
||||||
"@types/validator": "^13.15.0",
|
"@types/validator": "^13.15.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.32.0",
|
"@typescript-eslint/eslint-plugin": "^8.32.1",
|
||||||
"@typescript-eslint/parser": "^8.32.0",
|
"@typescript-eslint/parser": "^8.32.1",
|
||||||
"@vitejs/plugin-react-swc": "^3.9.0",
|
"@vitejs/plugin-react-swc": "^3.9.0",
|
||||||
"autoprefixer": "^10.4.21",
|
"autoprefixer": "^10.4.21",
|
||||||
"eslint": "^9.26.0",
|
"eslint": "^9.26.0",
|
||||||
|
@ -68,12 +73,13 @@
|
||||||
"eslint-plugin-react": "^7.37.5",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
"eslint-plugin-react-hooks": "^5.2.0",
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.20",
|
"eslint-plugin-react-refresh": "^0.4.20",
|
||||||
|
"globals": "^16.1.0",
|
||||||
"postcss": "^8.5.3",
|
"postcss": "^8.5.3",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.5.3",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||||
"tailwindcss": "^3.4.17",
|
"tailwindcss": "^4.1.7",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"vite": "^5.2.0",
|
"vite": "^6.3.5",
|
||||||
"vite-tsconfig-paths": "^5.1.4"
|
"vite-tsconfig-paths": "^5.1.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
export default {
|
export default {
|
||||||
plugins: {
|
plugins: {
|
||||||
tailwindcss: {},
|
|
||||||
autoprefixer: {},
|
autoprefixer: {},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -89,8 +89,8 @@ export default function Actionbar({
|
||||||
anchor="bottom start"
|
anchor="bottom start"
|
||||||
transition
|
transition
|
||||||
className={cx(
|
className={cx(
|
||||||
"z-10 flex w-[420px] origin-top flex-col !overflow-visible",
|
"z-10 flex w-[420px] origin-top flex-col overflow-visible!",
|
||||||
"flex origin-top flex-col transition duration-300 ease-out data-[closed]:translate-y-8 data-[closed]:opacity-0",
|
"flex origin-top flex-col transition duration-300 ease-out data-closed:translate-y-8 data-closed:opacity-0",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{({ open }) => {
|
{({ open }) => {
|
||||||
|
@ -131,8 +131,8 @@ export default function Actionbar({
|
||||||
anchor="bottom start"
|
anchor="bottom start"
|
||||||
transition
|
transition
|
||||||
className={cx(
|
className={cx(
|
||||||
"z-10 flex w-[420px] origin-top flex-col !overflow-visible",
|
"z-10 flex w-[420px] origin-top flex-col overflow-visible!",
|
||||||
"flex origin-top flex-col transition duration-300 ease-out data-[closed]:translate-y-8 data-[closed]:opacity-0",
|
"flex origin-top flex-col transition duration-300 ease-out data-closed:translate-y-8 data-closed:opacity-0",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{({ open }) => {
|
{({ open }) => {
|
||||||
|
@ -183,8 +183,8 @@ export default function Actionbar({
|
||||||
transitionProperty: "opacity",
|
transitionProperty: "opacity",
|
||||||
}}
|
}}
|
||||||
className={cx(
|
className={cx(
|
||||||
"z-10 flex w-[420px] origin-top flex-col !overflow-visible",
|
"z-10 flex w-[420px] origin-top flex-col overflow-visible!",
|
||||||
"flex origin-top flex-col transition duration-300 ease-out data-[closed]:translate-y-8 data-[closed]:opacity-0",
|
"flex origin-top flex-col transition duration-300 ease-out data-closed:translate-y-8 data-closed:opacity-0",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{({ open }) => {
|
{({ open }) => {
|
||||||
|
@ -226,8 +226,8 @@ export default function Actionbar({
|
||||||
anchor="bottom start"
|
anchor="bottom start"
|
||||||
transition
|
transition
|
||||||
className={cx(
|
className={cx(
|
||||||
"z-10 flex w-[420px] flex-col !overflow-visible",
|
"z-10 flex w-[420px] flex-col overflow-visible!",
|
||||||
"flex origin-top flex-col transition duration-300 ease-out data-[closed]:translate-y-8 data-[closed]:opacity-0",
|
"flex origin-top flex-col transition duration-300 ease-out data-closed:translate-y-8 data-closed:opacity-0",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{({ open }) => {
|
{({ open }) => {
|
||||||
|
@ -262,7 +262,23 @@ export default function Actionbar({
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{/* {useSettingsStore().actionBarCtrlAltDel && (
|
||||||
|
<div className="hidden lg:block">
|
||||||
|
<Button
|
||||||
|
size="XS"
|
||||||
|
theme="light"
|
||||||
|
text="Ctrl + Alt + Del"
|
||||||
|
LeadingIcon={FaLock}
|
||||||
|
onClick={() => {
|
||||||
|
sendKeyboardEvent(
|
||||||
|
[keys["Delete"]],
|
||||||
|
[modifiers["ControlLeft"], modifiers["AltLeft"]],
|
||||||
|
);
|
||||||
|
setTimeout(resetKeyboardState, 100);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)} */}
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
size="XS"
|
size="XS"
|
||||||
|
@ -274,7 +290,7 @@ export default function Actionbar({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="hidden items-center gap-x-2 lg:flex">
|
<div className="hidden items-center gap-x-2 lg:flex">
|
||||||
<div className="h-4 w-[1px] bg-slate-300 dark:bg-slate-600" />
|
<div className="h-4 w-px bg-slate-300 dark:bg-slate-600" />
|
||||||
<Button
|
<Button
|
||||||
size="XS"
|
size="XS"
|
||||||
theme="light"
|
theme="light"
|
||||||
|
|
|
@ -37,7 +37,7 @@ export default function AuthLayout({
|
||||||
<>
|
<>
|
||||||
<GridBackground />
|
<GridBackground />
|
||||||
|
|
||||||
<div className="grid min-h-screen grid-rows-layout">
|
<div className="grid min-h-screen grid-rows-(--grid-layout)">
|
||||||
<SimpleNavbar
|
<SimpleNavbar
|
||||||
logoHref="/"
|
logoHref="/"
|
||||||
actionElement={
|
actionElement={
|
||||||
|
|
|
@ -22,7 +22,7 @@ const AutoHeight = ({ children, ...props }: { children: React.ReactNode }) => {
|
||||||
{...props}
|
{...props}
|
||||||
height={height}
|
height={height}
|
||||||
duration={300}
|
duration={300}
|
||||||
contentClassName="h-fit"
|
contentClassName="h-fit p-px"
|
||||||
contentRef={contentDiv}
|
contentRef={contentDiv}
|
||||||
disableDisplayNone
|
disableDisplayNone
|
||||||
>
|
>
|
||||||
|
|
|
@ -16,7 +16,7 @@ const sizes = {
|
||||||
const themes = {
|
const themes = {
|
||||||
primary: cx(
|
primary: cx(
|
||||||
// Base styles
|
// Base styles
|
||||||
"bg-blue-700 dark:border-blue-600 border border-blue-900/60 text-white shadow",
|
"bg-blue-700 dark:border-blue-600 border border-blue-900/60 text-white shadow-sm",
|
||||||
// Hover states
|
// Hover states
|
||||||
"group-hover:bg-blue-800",
|
"group-hover:bg-blue-800",
|
||||||
// Active states
|
// Active states
|
||||||
|
@ -24,7 +24,7 @@ const themes = {
|
||||||
),
|
),
|
||||||
danger: cx(
|
danger: cx(
|
||||||
// Base styles
|
// Base styles
|
||||||
"bg-red-600 text-white border-red-700 shadow-sm shadow-red-200/80 dark:border-red-600 dark:shadow-red-900/20",
|
"bg-red-600 text-white border-red-700 shadow-xs shadow-red-200/80 dark:border-red-600 dark:shadow-red-900/20",
|
||||||
// Hover states
|
// Hover states
|
||||||
"group-hover:bg-red-700 group-hover:border-red-800 dark:group-hover:bg-red-700 dark:group-hover:border-red-600",
|
"group-hover:bg-red-700 group-hover:border-red-800 dark:group-hover:bg-red-700 dark:group-hover:border-red-600",
|
||||||
// Active states
|
// Active states
|
||||||
|
@ -34,7 +34,7 @@ const themes = {
|
||||||
),
|
),
|
||||||
light: cx(
|
light: cx(
|
||||||
// Base styles
|
// Base styles
|
||||||
"bg-white text-black border-slate-800/30 shadow dark:bg-slate-800 dark:border-slate-300/20 dark:text-white",
|
"bg-white text-black border-slate-800/30 shadow-xs dark:bg-slate-800 dark:border-slate-300/20 dark:text-white",
|
||||||
// Hover states
|
// Hover states
|
||||||
"group-hover:bg-blue-50/80 dark:group-hover:bg-slate-700",
|
"group-hover:bg-blue-50/80 dark:group-hover:bg-slate-700",
|
||||||
// Active states
|
// Active states
|
||||||
|
@ -44,7 +44,7 @@ const themes = {
|
||||||
),
|
),
|
||||||
lightDanger: cx(
|
lightDanger: cx(
|
||||||
// Base styles
|
// Base styles
|
||||||
"bg-white text-black border-red-400/60 shadow-sm",
|
"bg-white text-black border-red-400/60 shadow-xs",
|
||||||
// Hover states
|
// Hover states
|
||||||
"group-hover:bg-red-50/80",
|
"group-hover:bg-red-50/80",
|
||||||
// Active states
|
// Active states
|
||||||
|
@ -56,7 +56,7 @@ const themes = {
|
||||||
// Base styles
|
// Base styles
|
||||||
"bg-white/0 text-black border-transparent dark:text-white",
|
"bg-white/0 text-black border-transparent dark:text-white",
|
||||||
// Hover states
|
// Hover states
|
||||||
"group-hover:bg-white group-hover:border-slate-800/30 group-hover:shadow dark:group-hover:bg-slate-700 dark:group-hover:border-slate-600",
|
"group-hover:bg-white group-hover:border-slate-800/30 group-hover:shadow-sm dark:group-hover:bg-slate-700 dark:group-hover:border-slate-600",
|
||||||
// Active states
|
// Active states
|
||||||
"group-active:bg-slate-100/80",
|
"group-active:bg-slate-100/80",
|
||||||
),
|
),
|
||||||
|
@ -65,15 +65,15 @@ const themes = {
|
||||||
const btnVariants = cva({
|
const btnVariants = cva({
|
||||||
base: cx(
|
base: cx(
|
||||||
// Base styles
|
// Base styles
|
||||||
"border rounded select-none",
|
"border rounded-sm select-none",
|
||||||
// Size classes
|
// Size classes
|
||||||
"justify-center items-center shrink-0",
|
"justify-center items-center shrink-0",
|
||||||
// Transition classes
|
// Transition classes
|
||||||
"outline-none transition-all duration-200",
|
"outline-hidden transition-all duration-200",
|
||||||
// Text classes
|
// Text classes
|
||||||
"font-display text-center font-medium leading-tight",
|
"font-display text-center font-medium leading-tight",
|
||||||
// States
|
// States
|
||||||
"group-focus:outline-none group-focus:ring-2 group-focus:ring-offset-2 group-focus:ring-blue-700",
|
"group-focus:outline-hidden group-focus:ring-2 group-focus:ring-offset-2 group-focus:ring-blue-700",
|
||||||
"group-disabled:opacity-50 group-disabled:pointer-events-none",
|
"group-disabled:opacity-50 group-disabled:pointer-events-none",
|
||||||
),
|
),
|
||||||
|
|
||||||
|
@ -126,9 +126,9 @@ function ButtonContent(props: ButtonContentPropsType) {
|
||||||
<div
|
<div
|
||||||
className={cx(
|
className={cx(
|
||||||
"flex w-full min-w-0 items-center gap-x-1.5 text-center",
|
"flex w-full min-w-0 items-center gap-x-1.5 text-center",
|
||||||
textAlign === "left" ? "!text-left" : "",
|
textAlign === "left" ? "text-left!" : "",
|
||||||
textAlign === "center" ? "!text-center" : "",
|
textAlign === "center" ? "text-center!" : "",
|
||||||
textAlign === "right" ? "!text-right" : "",
|
textAlign === "right" ? "text-right!" : "",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
|
@ -175,7 +175,7 @@ type ButtonPropsType = Pick<
|
||||||
export const Button = React.forwardRef<HTMLButtonElement, ButtonPropsType>(
|
export const Button = React.forwardRef<HTMLButtonElement, ButtonPropsType>(
|
||||||
({ type, disabled, onClick, formNoValidate, loading, fetcher, ...props }, ref) => {
|
({ type, disabled, onClick, formNoValidate, loading, fetcher, ...props }, ref) => {
|
||||||
const classes = cx(
|
const classes = cx(
|
||||||
"group outline-none",
|
"group outline-hidden",
|
||||||
props.fullWidth ? "w-full" : "",
|
props.fullWidth ? "w-full" : "",
|
||||||
loading ? "pointer-events-none" : "",
|
loading ? "pointer-events-none" : "",
|
||||||
);
|
);
|
||||||
|
@ -215,8 +215,8 @@ type LinkPropsType = Pick<LinkProps, "to"> &
|
||||||
React.ComponentProps<typeof ButtonContent> & { disabled?: boolean };
|
React.ComponentProps<typeof ButtonContent> & { disabled?: boolean };
|
||||||
export const LinkButton = ({ to, ...props }: LinkPropsType) => {
|
export const LinkButton = ({ to, ...props }: LinkPropsType) => {
|
||||||
const classes = cx(
|
const classes = cx(
|
||||||
"group outline-none",
|
"group outline-hidden",
|
||||||
props.disabled ? "pointer-events-none !opacity-70" : "",
|
props.disabled ? "pointer-events-none opacity-70!" : "",
|
||||||
props.fullWidth ? "w-full" : "",
|
props.fullWidth ? "w-full" : "",
|
||||||
props.loading ? "pointer-events-none" : "",
|
props.loading ? "pointer-events-none" : "",
|
||||||
props.className,
|
props.className,
|
||||||
|
@ -241,8 +241,8 @@ type LabelPropsType = Pick<HTMLLabelElement, "htmlFor"> &
|
||||||
React.ComponentProps<typeof ButtonContent> & { disabled?: boolean };
|
React.ComponentProps<typeof ButtonContent> & { disabled?: boolean };
|
||||||
export const LabelButton = ({ htmlFor, ...props }: LabelPropsType) => {
|
export const LabelButton = ({ htmlFor, ...props }: LabelPropsType) => {
|
||||||
const classes = cx(
|
const classes = cx(
|
||||||
"group outline-none block cursor-pointer",
|
"group outline-hidden block cursor-pointer",
|
||||||
props.disabled ? "pointer-events-none !opacity-70" : "",
|
props.disabled ? "pointer-events-none opacity-70!" : "",
|
||||||
props.fullWidth ? "w-full" : "",
|
props.fullWidth ? "w-full" : "",
|
||||||
props.loading ? "pointer-events-none" : "",
|
props.loading ? "pointer-events-none" : "",
|
||||||
props.className,
|
props.className,
|
||||||
|
|
|
@ -17,8 +17,8 @@ export const GridCard = ({
|
||||||
return (
|
return (
|
||||||
<Card className={cx("overflow-hidden", cardClassName)}>
|
<Card className={cx("overflow-hidden", cardClassName)}>
|
||||||
<div className="relative h-full">
|
<div className="relative h-full">
|
||||||
<div className="absolute inset-0 z-0 h-full w-full bg-gradient-to-tr from-blue-50/30 to-blue-50/20 transition-colors duration-300 ease-in-out dark:from-slate-800/30 dark:to-slate-800/20" />
|
<div className="absolute inset-0 z-0 h-full w-full bg-linear-to-tr from-blue-50/30 to-blue-50/20 transition-colors duration-300 ease-in-out dark:from-slate-800/30 dark:to-slate-800/20" />
|
||||||
<div className="absolute inset-0 z-0 h-full w-full rotate-0 bg-grid-blue-100/[25%] dark:bg-grid-slate-700/[7%]" />
|
<div className="absolute inset-0 z-0 h-full w-full rotate-0 bg-grid-blue-100/25 dark:bg-grid-slate-700/7" />
|
||||||
<div className="isolate h-full">{children}</div>
|
<div className="isolate h-full">{children}</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
@ -30,7 +30,7 @@ const Card = forwardRef<HTMLDivElement, CardPropsType>(({ children, className },
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cx(
|
className={cx(
|
||||||
"w-full rounded border-none bg-white shadow outline outline-1 outline-slate-800/30 dark:bg-slate-800 dark:outline-slate-300/20",
|
"w-full rounded-sm border-none bg-white shadow-xs outline-1 outline-slate-800/30 dark:bg-slate-800 dark:outline-slate-300/20",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|
|
@ -12,10 +12,10 @@ const sizes = {
|
||||||
|
|
||||||
const checkboxVariants = cva({
|
const checkboxVariants = cva({
|
||||||
base: cx(
|
base: cx(
|
||||||
"block rounded",
|
"form-checkbox block rounded",
|
||||||
|
|
||||||
// Colors
|
// Colors
|
||||||
"border-slate-300 dark:border-slate-600 bg-slate-50 dark:bg-slate-800 text-blue-700 dark:text-blue-500 transition-colors",
|
"border-slate-300 dark:border-slate-600 bg-slate-50 dark:bg-slate-800 checked:accent-blue-700 checked:dark:accent-blue-500 transition-colors",
|
||||||
|
|
||||||
// Hover
|
// Hover
|
||||||
"hover:bg-slate-200/50 dark:hover:bg-slate-700/50",
|
"hover:bg-slate-200/50 dark:hover:bg-slate-700/50",
|
||||||
|
@ -24,7 +24,7 @@ const checkboxVariants = cva({
|
||||||
"active:bg-slate-200 dark:active:bg-slate-700",
|
"active:bg-slate-200 dark:active:bg-slate-700",
|
||||||
|
|
||||||
// Focus
|
// Focus
|
||||||
"focus:border-slate-300 dark:focus:border-slate-600 focus:outline-none focus:ring-2 focus:ring-blue-700 dark:focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-slate-900",
|
"focus:border-slate-300 dark:focus:border-slate-600 focus:outline-hidden focus:ring-2 focus:ring-blue-700 dark:focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-slate-900",
|
||||||
|
|
||||||
// Disabled
|
// Disabled
|
||||||
"disabled:pointer-events-none disabled:opacity-30",
|
"disabled:pointer-events-none disabled:opacity-30",
|
||||||
|
@ -41,7 +41,9 @@ const Checkbox = forwardRef<HTMLInputElement, CheckBoxProps>(function Checkbox(
|
||||||
ref,
|
ref,
|
||||||
) {
|
) {
|
||||||
const classes = checkboxVariants({ size });
|
const classes = checkboxVariants({ size });
|
||||||
return <input ref={ref} {...props} type="checkbox" className={clsx(classes, className)} />;
|
return (
|
||||||
|
<input ref={ref} {...props} type="checkbox" className={clsx(classes, className)} />
|
||||||
|
);
|
||||||
});
|
});
|
||||||
Checkbox.displayName = "Checkbox";
|
Checkbox.displayName = "Checkbox";
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,14 @@
|
||||||
import { useRef } from "react";
|
import { useRef } from "react";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { Combobox as HeadlessCombobox, ComboboxInput, ComboboxOption, ComboboxOptions } from "@headlessui/react";
|
import {
|
||||||
|
Combobox as HeadlessCombobox,
|
||||||
|
ComboboxInput,
|
||||||
|
ComboboxOption,
|
||||||
|
ComboboxOptions,
|
||||||
|
} from "@headlessui/react";
|
||||||
|
|
||||||
import { cva } from "@/cva.config";
|
import { cva } from "@/cva.config";
|
||||||
|
|
||||||
import Card from "./Card";
|
import Card from "./Card";
|
||||||
|
|
||||||
export interface ComboboxOption {
|
export interface ComboboxOption {
|
||||||
|
@ -22,7 +29,7 @@ const comboboxVariants = cva({
|
||||||
|
|
||||||
type BaseProps = React.ComponentProps<typeof HeadlessCombobox>;
|
type BaseProps = React.ComponentProps<typeof HeadlessCombobox>;
|
||||||
|
|
||||||
interface ComboboxProps extends Omit<BaseProps, 'displayValue'> {
|
interface ComboboxProps extends Omit<BaseProps, "displayValue"> {
|
||||||
displayValue: (option: ComboboxOption) => string;
|
displayValue: (option: ComboboxOption) => string;
|
||||||
onInputChange: (option: string) => void;
|
onInputChange: (option: string) => void;
|
||||||
options: () => ComboboxOption[];
|
options: () => ComboboxOption[];
|
||||||
|
@ -48,13 +55,10 @@ export function Combobox({
|
||||||
const classes = comboboxVariants({ size });
|
const classes = comboboxVariants({ size });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HeadlessCombobox
|
<HeadlessCombobox onChange={onChange} {...otherProps}>
|
||||||
onChange={onChange}
|
|
||||||
{...otherProps}
|
|
||||||
>
|
|
||||||
{() => (
|
{() => (
|
||||||
<>
|
<>
|
||||||
<Card className="w-auto !border border-solid !border-slate-800/30 shadow outline-0 dark:!border-slate-300/30">
|
<Card className="w-auto border! border-solid border-slate-800/30! shadow-xs outline-0 dark:border-slate-300/30!">
|
||||||
<ComboboxInput
|
<ComboboxInput
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
|
@ -73,30 +77,31 @@ export function Combobox({
|
||||||
"focus:outline-blue-600 focus:ring-2 focus:ring-blue-700 focus:ring-offset-2 dark:focus:outline-blue-500 dark:focus:ring-blue-500",
|
"focus:outline-blue-600 focus:ring-2 focus:ring-blue-700 focus:ring-offset-2 dark:focus:outline-blue-500 dark:focus:ring-blue-500",
|
||||||
|
|
||||||
// Disabled
|
// Disabled
|
||||||
disabled && "pointer-events-none select-none bg-slate-50 text-slate-500/80 dark:bg-slate-800 dark:text-slate-400/80 disabled:hover:bg-white dark:disabled:hover:bg-slate-800"
|
disabled &&
|
||||||
|
"pointer-events-none select-none bg-slate-50 text-slate-500/80 disabled:hover:bg-white dark:bg-slate-800 dark:text-slate-400/80 dark:disabled:hover:bg-slate-800",
|
||||||
)}
|
)}
|
||||||
placeholder={disabled ? disabledMessage : placeholder}
|
placeholder={disabled ? disabledMessage : placeholder}
|
||||||
displayValue={displayValue}
|
displayValue={displayValue}
|
||||||
onChange={(event) => onInputChange(event.target.value)}
|
onChange={event => onInputChange(event.target.value)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{options().length > 0 && (
|
{options().length > 0 && (
|
||||||
<ComboboxOptions className="absolute left-0 z-[100] mt-1 w-full max-h-60 overflow-auto rounded-md bg-white py-1 text-sm shadow-lg ring-1 ring-black/5 dark:bg-slate-800 dark:ring-slate-700 hide-scrollbar">
|
<ComboboxOptions className="hide-scrollbar absolute left-0 z-100 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-sm shadow-lg ring-1 ring-black/5 dark:bg-slate-800 dark:ring-slate-700">
|
||||||
{options().map((option) => (
|
{options().map(option => (
|
||||||
<ComboboxOption
|
<ComboboxOption
|
||||||
key={option.value}
|
key={option.value}
|
||||||
value={option}
|
value={option}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
// General styling
|
// General styling
|
||||||
"cursor-default select-none py-2 px-4",
|
"cursor-default select-none px-4 py-2",
|
||||||
|
|
||||||
// Hover and active states
|
// Hover and active states
|
||||||
"hover:bg-blue-50/80 ui-active:bg-blue-50/80 ui-active:text-blue-900",
|
"hover:bg-blue-50/80 ui-active:bg-blue-50/80 ui-active:text-blue-900",
|
||||||
|
|
||||||
// Dark mode
|
// Dark mode
|
||||||
"dark:text-slate-300 dark:hover:bg-slate-700 dark:ui-active:bg-slate-700 dark:ui-active:text-blue-200"
|
"dark:text-slate-300 dark:hover:bg-slate-700 dark:ui-active:bg-slate-700 dark:ui-active:text-blue-200",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{option.label}
|
{option.label}
|
||||||
|
@ -106,10 +111,8 @@ export function Combobox({
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{options().length === 0 && inputRef.current?.value && (
|
{options().length === 0 && inputRef.current?.value && (
|
||||||
<div className="absolute left-0 z-[100] mt-1 w-full rounded-md bg-white dark:bg-slate-800 py-2 px-4 text-sm shadow-lg ring-1 ring-black/5 dark:ring-slate-700">
|
<div className="absolute left-0 z-100 mt-1 w-full rounded-md bg-white px-4 py-2 text-sm shadow-lg ring-1 ring-black/5 dark:bg-slate-800 dark:ring-slate-700">
|
||||||
<div className="text-slate-500 dark:text-slate-400">
|
<div className="text-slate-500 dark:text-slate-400">{emptyMessage}</div>
|
||||||
{emptyMessage}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -1,7 +1,12 @@
|
||||||
import { ExclamationTriangleIcon, CheckCircleIcon, InformationCircleIcon } from "@heroicons/react/24/outline";
|
import {
|
||||||
import { cx } from "@/cva.config";
|
CheckCircleIcon,
|
||||||
|
ExclamationTriangleIcon,
|
||||||
|
InformationCircleIcon,
|
||||||
|
} from "@heroicons/react/24/outline";
|
||||||
|
|
||||||
import { Button } from "@/components/Button";
|
import { Button } from "@/components/Button";
|
||||||
import Modal from "@/components/Modal";
|
import Modal from "@/components/Modal";
|
||||||
|
import { cx } from "@/cva.config";
|
||||||
|
|
||||||
type Variant = "danger" | "success" | "warning" | "info";
|
type Variant = "danger" | "success" | "warning" | "info";
|
||||||
|
|
||||||
|
@ -9,7 +14,7 @@ interface ConfirmDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: React.ReactNode;
|
||||||
variant?: Variant;
|
variant?: Variant;
|
||||||
confirmText?: string;
|
confirmText?: string;
|
||||||
cancelText?: string | null;
|
cancelText?: string | null;
|
||||||
|
@ -42,12 +47,15 @@ const variantConfig = {
|
||||||
iconBgClass: "bg-blue-100",
|
iconBgClass: "bg-blue-100",
|
||||||
buttonTheme: "primary",
|
buttonTheme: "primary",
|
||||||
},
|
},
|
||||||
} as Record<Variant, {
|
} as Record<
|
||||||
|
Variant,
|
||||||
|
{
|
||||||
icon: React.ElementType;
|
icon: React.ElementType;
|
||||||
iconClass: string;
|
iconClass: string;
|
||||||
iconBgClass: string;
|
iconBgClass: string;
|
||||||
buttonTheme: "danger" | "primary" | "blank" | "light" | "lightDanger";
|
buttonTheme: "danger" | "primary" | "blank" | "light" | "lightDanger";
|
||||||
}>;
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
export function ConfirmDialog({
|
export function ConfirmDialog({
|
||||||
open,
|
open,
|
||||||
|
@ -65,14 +73,19 @@ export function ConfirmDialog({
|
||||||
return (
|
return (
|
||||||
<Modal open={open} onClose={onClose}>
|
<Modal open={open} onClose={onClose}>
|
||||||
<div className="mx-auto max-w-xl px-4 transition-all duration-300 ease-in-out">
|
<div className="mx-auto max-w-xl px-4 transition-all duration-300 ease-in-out">
|
||||||
<div className="relative w-full overflow-hidden rounded-lg bg-white p-6 text-left align-middle shadow-xl transition-all dark:bg-slate-800 pointer-events-auto">
|
<div className="pointer-events-auto relative w-full overflow-hidden rounded-lg bg-white p-6 text-left align-middle shadow-xl transition-all dark:bg-slate-800">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="sm:flex sm:items-start">
|
<div className="sm:flex sm:items-start">
|
||||||
<div className={cx("mx-auto flex size-12 shrink-0 items-center justify-center rounded-full sm:mx-0 sm:size-10", iconBgClass)}>
|
<div
|
||||||
|
className={cx(
|
||||||
|
"mx-auto flex size-12 shrink-0 items-center justify-center rounded-full sm:mx-0 sm:size-10",
|
||||||
|
iconBgClass,
|
||||||
|
)}
|
||||||
|
>
|
||||||
<Icon aria-hidden="true" className={cx("size-6", iconClass)} />
|
<Icon aria-hidden="true" className={cx("size-6", iconClass)} />
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
||||||
<h2 className="text-lg font-bold leading-tight text-black dark:text-white">
|
<h2 className="text-lg leading-tight font-bold text-black dark:text-white">
|
||||||
{title}
|
{title}
|
||||||
</h2>
|
</h2>
|
||||||
<div className="mt-2 text-sm leading-snug text-slate-600 dark:text-slate-400">
|
<div className="mt-2 text-sm leading-snug text-slate-600 dark:text-slate-400">
|
||||||
|
@ -83,12 +96,7 @@ export function ConfirmDialog({
|
||||||
|
|
||||||
<div className="flex justify-end gap-x-2">
|
<div className="flex justify-end gap-x-2">
|
||||||
{cancelText && (
|
{cancelText && (
|
||||||
<Button
|
<Button size="SM" theme="blank" text={cancelText} onClick={onClose} />
|
||||||
size="SM"
|
|
||||||
theme="blank"
|
|
||||||
text={cancelText}
|
|
||||||
onClick={onClose}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
size="SM"
|
size="SM"
|
||||||
|
|
|
@ -0,0 +1,212 @@
|
||||||
|
import { LuRefreshCcw } from "react-icons/lu";
|
||||||
|
|
||||||
|
import { Button } from "@/components/Button";
|
||||||
|
import { GridCard } from "@/components/Card";
|
||||||
|
import { LifeTimeLabel } from "@/routes/devices.$id.settings.network";
|
||||||
|
import { NetworkState } from "@/hooks/stores";
|
||||||
|
|
||||||
|
export default function DhcpLeaseCard({
|
||||||
|
networkState,
|
||||||
|
setShowRenewLeaseConfirm,
|
||||||
|
}: {
|
||||||
|
networkState: NetworkState;
|
||||||
|
setShowRenewLeaseConfirm: (show: boolean) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<GridCard>
|
||||||
|
<div className="animate-fadeIn p-4 opacity-0 animation-duration-500 text-black dark:text-white">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<h3 className="text-base font-bold text-slate-900 dark:text-white">
|
||||||
|
DHCP Lease Information
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="flex gap-x-6 gap-y-2">
|
||||||
|
<div className="flex-1 space-y-2">
|
||||||
|
{networkState?.dhcp_lease?.ip && (
|
||||||
|
<div className="flex justify-between border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||||
|
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||||
|
IP Address
|
||||||
|
</span>
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
{networkState?.dhcp_lease?.ip}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{networkState?.dhcp_lease?.netmask && (
|
||||||
|
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||||
|
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||||
|
Subnet Mask
|
||||||
|
</span>
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
{networkState?.dhcp_lease?.netmask}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{networkState?.dhcp_lease?.dns && (
|
||||||
|
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||||
|
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||||
|
DNS Servers
|
||||||
|
</span>
|
||||||
|
<span className="text-right text-sm font-medium">
|
||||||
|
{networkState?.dhcp_lease?.dns.map(dns => <div key={dns}>{dns}</div>)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{networkState?.dhcp_lease?.broadcast && (
|
||||||
|
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||||
|
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||||
|
Broadcast
|
||||||
|
</span>
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
{networkState?.dhcp_lease?.broadcast}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{networkState?.dhcp_lease?.domain && (
|
||||||
|
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||||
|
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||||
|
Domain
|
||||||
|
</span>
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
{networkState?.dhcp_lease?.domain}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{networkState?.dhcp_lease?.ntp_servers &&
|
||||||
|
networkState?.dhcp_lease?.ntp_servers.length > 0 && (
|
||||||
|
<div className="flex justify-between gap-x-8 border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||||
|
<div className="w-full grow text-sm text-slate-600 dark:text-slate-400">
|
||||||
|
NTP Servers
|
||||||
|
</div>
|
||||||
|
<div className="shrink text-right text-sm font-medium">
|
||||||
|
{networkState?.dhcp_lease?.ntp_servers.map(server => (
|
||||||
|
<div key={server}>{server}</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{networkState?.dhcp_lease?.hostname && (
|
||||||
|
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||||
|
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||||
|
Hostname
|
||||||
|
</span>
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
{networkState?.dhcp_lease?.hostname}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 space-y-2">
|
||||||
|
{networkState?.dhcp_lease?.routers &&
|
||||||
|
networkState?.dhcp_lease?.routers.length > 0 && (
|
||||||
|
<div className="flex justify-between pt-2">
|
||||||
|
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||||
|
Gateway
|
||||||
|
</span>
|
||||||
|
<span className="text-right text-sm font-medium">
|
||||||
|
{networkState?.dhcp_lease?.routers.map(router => (
|
||||||
|
<div key={router}>{router}</div>
|
||||||
|
))}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{networkState?.dhcp_lease?.server_id && (
|
||||||
|
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||||
|
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||||
|
DHCP Server
|
||||||
|
</span>
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
{networkState?.dhcp_lease?.server_id}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{networkState?.dhcp_lease?.lease_expiry && (
|
||||||
|
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||||
|
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||||
|
Lease Expires
|
||||||
|
</span>
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
<LifeTimeLabel
|
||||||
|
lifetime={`${networkState?.dhcp_lease?.lease_expiry}`}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{networkState?.dhcp_lease?.mtu && (
|
||||||
|
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||||
|
<span className="text-sm text-slate-600 dark:text-slate-400">MTU</span>
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
{networkState?.dhcp_lease?.mtu}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{networkState?.dhcp_lease?.ttl && (
|
||||||
|
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||||
|
<span className="text-sm text-slate-600 dark:text-slate-400">TTL</span>
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
{networkState?.dhcp_lease?.ttl}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{networkState?.dhcp_lease?.bootp_next_server && (
|
||||||
|
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||||
|
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||||
|
Boot Next Server
|
||||||
|
</span>
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
{networkState?.dhcp_lease?.bootp_next_server}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{networkState?.dhcp_lease?.bootp_server_name && (
|
||||||
|
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||||
|
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||||
|
Boot Server Name
|
||||||
|
</span>
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
{networkState?.dhcp_lease?.bootp_server_name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{networkState?.dhcp_lease?.bootp_file && (
|
||||||
|
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||||
|
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||||
|
Boot File
|
||||||
|
</span>
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
{networkState?.dhcp_lease?.bootp_file}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
size="SM"
|
||||||
|
theme="light"
|
||||||
|
className="text-red-500"
|
||||||
|
text="Renew DHCP Lease"
|
||||||
|
LeadingIcon={LuRefreshCcw}
|
||||||
|
onClick={() => setShowRenewLeaseConfirm(true)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</GridCard>
|
||||||
|
);
|
||||||
|
}
|
|
@ -30,7 +30,7 @@ export default function EmptyCard({
|
||||||
<div className="max-w-[90%] space-y-1.5 text-center md:max-w-[60%]">
|
<div className="max-w-[90%] space-y-1.5 text-center md:max-w-[60%]">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{IconElm && (
|
{IconElm && (
|
||||||
<IconElm className="mx-auto h-6 w-6 text-blue-600 dark:text-blue-400" />
|
<IconElm className="mx-auto h-5 w-5 text-blue-600 dark:text-blue-600" />
|
||||||
)}
|
)}
|
||||||
<h4 className="text-base font-bold leading-none text-black dark:text-white">
|
<h4 className="text-base font-bold leading-none text-black dark:text-white">
|
||||||
{headline}
|
{headline}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
export default function GridBackground() {
|
export default function GridBackground() {
|
||||||
return (
|
return (
|
||||||
<div className="absolute w-screen h-screen overflow-hidden isolate opacity-60">
|
<div className="absolute isolate h-screen w-screen overflow-hidden opacity-60">
|
||||||
<svg
|
<svg
|
||||||
className="absolute inset-x-0 top-0 -z-10 h-[64rem] w-full stroke-gray-300 [mask-image:radial-gradient(32rem_32rem_at_center,white,transparent)] dark:stroke-slate-300/20"
|
className="absolute inset-x-0 top-0 -z-10 h-full w-full mask-radial-[32rem_32rem] mask-radial-from-white mask-radial-to-transparent mask-radial-at-center stroke-gray-300 dark:stroke-slate-300/20"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
<defs>
|
<defs>
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
import { Fragment, useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { ArrowLeftEndOnRectangleIcon, ChevronDownIcon } from "@heroicons/react/16/solid";
|
import { ArrowLeftEndOnRectangleIcon, ChevronDownIcon } from "@heroicons/react/16/solid";
|
||||||
import { Menu, MenuButton } from "@headlessui/react";
|
import { Button, Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react";
|
||||||
import { LuMonitorSmartphone } from "react-icons/lu";
|
import { LuMonitorSmartphone } from "react-icons/lu";
|
||||||
|
|
||||||
import Container from "@/components/Container";
|
import Container from "@/components/Container";
|
||||||
import Card from "@/components/Card";
|
import Card from "@/components/Card";
|
||||||
import { cx } from "@/cva.config";
|
|
||||||
import { useHidStore, useRTCStore, useUserStore } from "@/hooks/stores";
|
import { useHidStore, useRTCStore, useUserStore } from "@/hooks/stores";
|
||||||
import LogoBlueIcon from "@/assets/logo-blue.svg";
|
import LogoBlueIcon from "@/assets/logo-blue.svg";
|
||||||
import LogoWhiteIcon from "@/assets/logo-white.svg";
|
import LogoWhiteIcon from "@/assets/logo-white.svg";
|
||||||
|
@ -17,7 +16,7 @@ import { CLOUD_API, DEVICE_API } from "@/ui.config";
|
||||||
import api from "../api";
|
import api from "../api";
|
||||||
import { isOnDevice } from "../main";
|
import { isOnDevice } from "../main";
|
||||||
|
|
||||||
import { Button, LinkButton } from "./Button";
|
import { LinkButton } from "./Button";
|
||||||
|
|
||||||
interface NavbarProps {
|
interface NavbarProps {
|
||||||
isLoggedIn: boolean;
|
isLoggedIn: boolean;
|
||||||
|
@ -51,8 +50,12 @@ export default function DashboardNavbar({
|
||||||
|
|
||||||
const usbState = useHidStore(state => state.usbState);
|
const usbState = useHidStore(state => state.usbState);
|
||||||
|
|
||||||
|
// for testing
|
||||||
|
//userEmail = "user@example.org";
|
||||||
|
//picture = "https://placehold.co/32x32"
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full select-none border-b border-b-slate-800/20 bg-white dark:border-b-slate-300/20 dark:bg-slate-900">
|
<div className="w-full border-b border-b-slate-800/20 bg-white select-none dark:border-b-slate-300/20 dark:bg-slate-900">
|
||||||
<Container>
|
<Container>
|
||||||
<div className="flex h-14 items-center justify-between">
|
<div className="flex h-14 items-center justify-between">
|
||||||
<div className="flex shrink-0 items-center gap-x-8">
|
<div className="flex shrink-0 items-center gap-x-8">
|
||||||
|
@ -78,8 +81,9 @@ export default function DashboardNavbar({
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-full items-center justify-end gap-x-2">
|
<div className="flex w-full items-center justify-end gap-x-2">
|
||||||
<div className="flex shrink-0 items-center space-x-4">
|
<div className="flex shrink-0 items-center space-x-4">
|
||||||
|
<div className="hidden items-stretch gap-x-2 md:flex">
|
||||||
{showConnectionStatus && (
|
{showConnectionStatus && (
|
||||||
<div className="hidden items-center gap-x-2 md:flex">
|
<>
|
||||||
<div className="w-[159px]">
|
<div className="w-[159px]">
|
||||||
<PeerConnectionStatusCard
|
<PeerConnectionStatusCard
|
||||||
state={peerConnectionState}
|
state={peerConnectionState}
|
||||||
|
@ -92,75 +96,70 @@ export default function DashboardNavbar({
|
||||||
peerConnectionState={peerConnectionState}
|
peerConnectionState={peerConnectionState}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
)}
|
)}
|
||||||
{isLoggedIn ? (
|
{isLoggedIn ? (
|
||||||
<>
|
<>
|
||||||
<hr className="h-[20px] w-[1px] border-none bg-slate-800/20 dark:bg-slate-300/20" />
|
<hr className="h-[20px] w-px self-center border-none bg-slate-800/20 dark:bg-slate-300/20" />
|
||||||
<Menu as="div" className="relative inline-block text-left">
|
<div className="relative inline-block text-left">
|
||||||
<div>
|
<Menu>
|
||||||
<MenuButton as={Fragment}>
|
<MenuButton className="h-full">
|
||||||
<Button
|
<Button className="flex h-full items-center gap-x-3 rounded-md border border-slate-800/20 bg-white px-2 py-1.5 dark:border-slate-600 dark:bg-slate-800 dark:text-white">
|
||||||
theme="blank"
|
{picture ? (
|
||||||
size="SM"
|
|
||||||
text={
|
|
||||||
<>
|
|
||||||
{picture ? <></> : userEmail}
|
|
||||||
<ChevronDownIcon className="h-4 w-4 shrink-0 text-slate-900 dark:text-white" />
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
LeadingIcon={({ className }) =>
|
|
||||||
picture && (
|
|
||||||
<img
|
<img
|
||||||
src={picture}
|
src={picture}
|
||||||
alt="Avatar"
|
alt="Avatar"
|
||||||
className={cx(
|
className="size-6 rounded-full border-2 border-transparent transition-colors group-hover:border-blue-700"
|
||||||
className,
|
|
||||||
"h-8 w-8 rounded-full border-2 border-transparent transition-colors group-hover:border-blue-700",
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
|
) : userEmail ? (
|
||||||
|
<span className="font-display max-w-[200px] truncate text-sm/6 font-semibold">
|
||||||
|
{userEmail}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
<ChevronDownIcon className="size-4 shrink-0 text-slate-900 dark:text-white" />
|
||||||
|
</Button>
|
||||||
</MenuButton>
|
</MenuButton>
|
||||||
</div>
|
<MenuItems
|
||||||
|
transition
|
||||||
<Menu.Items className="absolute right-0 z-50 mt-2 w-56 origin-top-right focus:outline-none">
|
anchor="bottom end"
|
||||||
|
className="right-0 mt-1 w-56 origin-top-right p-px focus:outline-hidden data-closed:opacity-0"
|
||||||
|
>
|
||||||
|
<MenuItem>
|
||||||
<Card className="overflow-hidden">
|
<Card className="overflow-hidden">
|
||||||
<div className="space-y-1 p-1 dark:text-white">
|
|
||||||
{userEmail && (
|
{userEmail && (
|
||||||
|
<div className="space-y-1 p-1 dark:text-white">
|
||||||
<div className="border-b border-b-slate-800/20 dark:border-slate-300/20">
|
<div className="border-b border-b-slate-800/20 dark:border-slate-300/20">
|
||||||
<Menu.Item>
|
|
||||||
<div className="p-2">
|
<div className="p-2">
|
||||||
<div className="font-display text-xs">Logged in as</div>
|
<div className="font-display text-xs">
|
||||||
<div className="w-[200px] truncate font-display text-sm font-semibold">
|
Logged in as
|
||||||
|
</div>
|
||||||
|
<div className="font-display max-w-[200px] truncate text-sm font-semibold">
|
||||||
{userEmail}
|
{userEmail}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Menu.Item>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div>
|
<div
|
||||||
<Menu.Item>
|
className="space-y-1 p-1 dark:text-white"
|
||||||
<div onClick={onLogout}>
|
onClick={onLogout}
|
||||||
<button className="block w-full">
|
>
|
||||||
<div className="flex items-center gap-x-2 rounded-md px-2 py-1.5 text-sm transition-colors hover:bg-slate-100 dark:hover:bg-slate-700">
|
<button className="group flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm transition-colors hover:bg-slate-100 dark:hover:bg-slate-700">
|
||||||
<ArrowLeftEndOnRectangleIcon className="h-4 w-4" />
|
<ArrowLeftEndOnRectangleIcon className="size-4" />
|
||||||
<div className="font-display">Log out</div>
|
<div className="font-display">Log out</div>
|
||||||
</div>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</Menu.Item>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
</Card>
|
||||||
</Menu.Items>
|
</MenuItem>
|
||||||
|
</MenuItems>
|
||||||
</Menu>
|
</Menu>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</Container>
|
</Container>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -28,6 +28,7 @@ export default function InfoBar() {
|
||||||
const rpcDataChannel = useRTCStore(state => state.rpcDataChannel);
|
const rpcDataChannel = useRTCStore(state => state.rpcDataChannel);
|
||||||
|
|
||||||
const settings = useSettingsStore();
|
const settings = useSettingsStore();
|
||||||
|
const showPressedKeys = useSettingsStore(state => state.showPressedKeys);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!rpcDataChannel) return;
|
if (!rpcDataChannel) return;
|
||||||
|
@ -36,9 +37,9 @@ export default function InfoBar() {
|
||||||
console.log(`Error on DataChannel '${rpcDataChannel.label}': ${e}`);
|
console.log(`Error on DataChannel '${rpcDataChannel.label}': ${e}`);
|
||||||
}, [rpcDataChannel]);
|
}, [rpcDataChannel]);
|
||||||
|
|
||||||
const isCapsLockActive = useHidStore(state => state.isCapsLockActive);
|
const keyboardLedState = useHidStore(state => state.keyboardLedState);
|
||||||
const isNumLockActive = useHidStore(state => state.isNumLockActive);
|
const keyboardLedStateSyncAvailable = useHidStore(state => state.keyboardLedStateSyncAvailable);
|
||||||
const isScrollLockActive = useHidStore(state => state.isScrollLockActive);
|
const keyboardLedSync = useSettingsStore(state => state.keyboardLedSync);
|
||||||
|
|
||||||
const isTurnServerInUse = useRTCStore(state => state.isTurnServerInUse);
|
const isTurnServerInUse = useRTCStore(state => state.isTurnServerInUse);
|
||||||
|
|
||||||
|
@ -97,6 +98,7 @@ export default function InfoBar() {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{showPressedKeys && (
|
||||||
<div className="flex items-center gap-x-1">
|
<div className="flex items-center gap-x-1">
|
||||||
<span className="text-xs font-semibold">Keys:</span>
|
<span className="text-xs font-semibold">Keys:</span>
|
||||||
<h2 className="text-xs">
|
<h2 className="text-xs">
|
||||||
|
@ -110,6 +112,7 @@ export default function InfoBar() {
|
||||||
].join(", ")}
|
].join(", ")}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center divide-x first:divide-l divide-slate-800/20 dark:divide-slate-300/20">
|
<div className="flex items-center divide-x first:divide-l divide-slate-800/20 dark:divide-slate-300/20">
|
||||||
|
@ -118,10 +121,24 @@ export default function InfoBar() {
|
||||||
Relayed by Cloudflare
|
Relayed by Cloudflare
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{keyboardLedStateSyncAvailable ? (
|
||||||
<div
|
<div
|
||||||
className={cx(
|
className={cx(
|
||||||
"shrink-0 p-1 px-1.5 text-xs",
|
"shrink-0 p-1 px-1.5 text-xs",
|
||||||
isCapsLockActive
|
keyboardLedSync !== "browser"
|
||||||
|
? "text-black dark:text-white"
|
||||||
|
: "text-slate-800/20 dark:text-slate-300/20",
|
||||||
|
)}
|
||||||
|
title={"Your keyboard LED state is managed by" + (keyboardLedSync === "browser" ? " the browser" : " the host")}
|
||||||
|
>
|
||||||
|
{keyboardLedSync === "browser" ? "Browser" : "Host"}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<div
|
||||||
|
className={cx(
|
||||||
|
"shrink-0 p-1 px-1.5 text-xs",
|
||||||
|
keyboardLedState?.caps_lock
|
||||||
? "text-black dark:text-white"
|
? "text-black dark:text-white"
|
||||||
: "text-slate-800/20 dark:text-slate-300/20",
|
: "text-slate-800/20 dark:text-slate-300/20",
|
||||||
)}
|
)}
|
||||||
|
@ -131,7 +148,7 @@ export default function InfoBar() {
|
||||||
<div
|
<div
|
||||||
className={cx(
|
className={cx(
|
||||||
"shrink-0 p-1 px-1.5 text-xs",
|
"shrink-0 p-1 px-1.5 text-xs",
|
||||||
isNumLockActive
|
keyboardLedState?.num_lock
|
||||||
? "text-black dark:text-white"
|
? "text-black dark:text-white"
|
||||||
: "text-slate-800/20 dark:text-slate-300/20",
|
: "text-slate-800/20 dark:text-slate-300/20",
|
||||||
)}
|
)}
|
||||||
|
@ -141,13 +158,23 @@ export default function InfoBar() {
|
||||||
<div
|
<div
|
||||||
className={cx(
|
className={cx(
|
||||||
"shrink-0 p-1 px-1.5 text-xs",
|
"shrink-0 p-1 px-1.5 text-xs",
|
||||||
isScrollLockActive
|
keyboardLedState?.scroll_lock
|
||||||
? "text-black dark:text-white"
|
? "text-black dark:text-white"
|
||||||
: "text-slate-800/20 dark:text-slate-300/20",
|
: "text-slate-800/20 dark:text-slate-300/20",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
Scroll Lock
|
Scroll Lock
|
||||||
</div>
|
</div>
|
||||||
|
{keyboardLedState?.compose ? (
|
||||||
|
<div className="shrink-0 p-1 px-1.5 text-xs">
|
||||||
|
Compose
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{keyboardLedState?.kana ? (
|
||||||
|
<div className="shrink-0 p-1 px-1.5 text-xs">
|
||||||
|
Kana
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -44,7 +44,7 @@ const InputField = forwardRef<HTMLInputElement, InputFieldProps>(function InputF
|
||||||
"[&:has(:user-invalid)]:ring-2 [&:has(:user-invalid)]:ring-red-600 [&:has(:user-invalid)]:ring-offset-2",
|
"[&:has(:user-invalid)]:ring-2 [&:has(:user-invalid)]:ring-red-600 [&:has(:user-invalid)]:ring-offset-2",
|
||||||
|
|
||||||
// Focus Within
|
// Focus Within
|
||||||
"focus-within:border-slate-300 dark:focus-within:border-slate-600 focus-within:outline-none focus-within:ring-2 focus-within:ring-blue-700 focus-within:ring-offset-2",
|
"focus-within:border-slate-300 dark:focus-within:border-slate-600 focus-within:outline-hidden focus-within:ring-2 focus-within:ring-blue-700 focus-within:ring-offset-2",
|
||||||
|
|
||||||
// Disabled Within
|
// Disabled Within
|
||||||
"disabled-within:pointer-events-none disabled-within:select-none disabled-within:bg-slate-50 dark:disabled-within:bg-slate-800 disabled-within:text-slate-500/80",
|
"disabled-within:pointer-events-none disabled-within:select-none disabled-within:bg-slate-50 dark:disabled-within:bg-slate-800 disabled-within:text-slate-500/80",
|
||||||
|
|
|
@ -0,0 +1,93 @@
|
||||||
|
import { NetworkState } from "../hooks/stores";
|
||||||
|
import { LifeTimeLabel } from "../routes/devices.$id.settings.network";
|
||||||
|
|
||||||
|
import { GridCard } from "./Card";
|
||||||
|
|
||||||
|
export default function Ipv6NetworkCard({
|
||||||
|
networkState,
|
||||||
|
}: {
|
||||||
|
networkState: NetworkState;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<GridCard>
|
||||||
|
<div className="animate-fadeIn p-4 text-black opacity-0 animation-duration-500 dark:text-white">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-base font-bold text-slate-900 dark:text-white">
|
||||||
|
IPv6 Information
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-x-6 gap-y-2">
|
||||||
|
{networkState?.dhcp_lease?.ip && (
|
||||||
|
<div className="flex flex-col justify-between">
|
||||||
|
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||||
|
Link-local
|
||||||
|
</span>
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
{networkState?.ipv6_link_local}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3 pt-2">
|
||||||
|
{networkState?.ipv6_addresses && networkState?.ipv6_addresses.length > 0 && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<h4 className="text-sm font-semibold">IPv6 Addresses</h4>
|
||||||
|
{networkState.ipv6_addresses.map(
|
||||||
|
addr => (
|
||||||
|
<div
|
||||||
|
key={addr.address}
|
||||||
|
className="rounded-md rounded-l-none border border-slate-500/10 border-l-blue-700/50 bg-white p-4 pl-4 backdrop-blur-sm dark:bg-transparent"
|
||||||
|
>
|
||||||
|
<div className="grid grid-cols-2 gap-x-8 gap-y-4">
|
||||||
|
<div className="col-span-2 flex flex-col justify-between">
|
||||||
|
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||||
|
Address
|
||||||
|
</span>
|
||||||
|
<span className="text-sm font-medium">{addr.address}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{addr.valid_lifetime && (
|
||||||
|
<div className="flex flex-col justify-between">
|
||||||
|
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||||
|
Valid Lifetime
|
||||||
|
</span>
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
{addr.valid_lifetime === "" ? (
|
||||||
|
<span className="text-slate-400 dark:text-slate-600">
|
||||||
|
N/A
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<LifeTimeLabel lifetime={`${addr.valid_lifetime}`} />
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{addr.preferred_lifetime && (
|
||||||
|
<div className="flex flex-col justify-between">
|
||||||
|
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||||
|
Preferred Lifetime
|
||||||
|
</span>
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
{addr.preferred_lifetime === "" ? (
|
||||||
|
<span className="text-slate-400 dark:text-slate-600">
|
||||||
|
N/A
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<LifeTimeLabel lifetime={`${addr.preferred_lifetime}`} />
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</GridCard>
|
||||||
|
);
|
||||||
|
}
|
|
@ -78,7 +78,7 @@ export default function KvmCard({
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-[1px] bg-slate-800/20 dark:bg-slate-300/20" />
|
<div className="h-px bg-slate-800/20 dark:bg-slate-300/20" />
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<div>
|
<div>
|
||||||
{online ? (
|
{online ? (
|
||||||
|
@ -111,9 +111,9 @@ export default function KvmCard({
|
||||||
|
|
||||||
<MenuItems
|
<MenuItems
|
||||||
transition
|
transition
|
||||||
className="data-[closed]:scale-95 data-[closed]:transform data-[closed]:opacity-0 data-[enter]:duration-100 data-[leave]:duration-75 data-[enter]:ease-out data-[leave]:ease-in"
|
className="data-closed:scale-95 data-closed:transform data-closed:opacity-0 data-enter:duration-100 data-leave:duration-75 data-enter:ease-out data-leave:ease-in"
|
||||||
>
|
>
|
||||||
<Card className="absolute right-0 z-10 w-56 px-1 mt-2 transition origin-top-right ring-1 ring-black ring-opacity-5 focus:outline-none">
|
<Card className="absolute right-0 z-10 w-56 px-1 mt-2 transition origin-top-right ring-1 ring-black/50 focus:outline-hidden">
|
||||||
<div className="divide-y divide-slate-800/20 dark:divide-slate-300/20">
|
<div className="divide-y divide-slate-800/20 dark:divide-slate-300/20">
|
||||||
<MenuItem>
|
<MenuItem>
|
||||||
<div>
|
<div>
|
||||||
|
|
|
@ -7,7 +7,7 @@ export default function LoadingSpinner({
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
className={clsx(className, "flex-shrink-0 animate-spin p-[2px]")}
|
className={clsx(className, "shrink-0 animate-spin p-[2px]")}
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
fill="none"
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import { LuPlus } from "react-icons/lu";
|
import { LuPlus } from "react-icons/lu";
|
||||||
|
|
||||||
import { KeySequence } from "@/hooks/stores";
|
import { KeySequence } from "@/hooks/stores";
|
||||||
|
@ -7,16 +6,23 @@ import { Button } from "@/components/Button";
|
||||||
import { InputFieldWithLabel, FieldError } from "@/components/InputField";
|
import { InputFieldWithLabel, FieldError } from "@/components/InputField";
|
||||||
import Fieldset from "@/components/Fieldset";
|
import Fieldset from "@/components/Fieldset";
|
||||||
import { MacroStepCard } from "@/components/MacroStepCard";
|
import { MacroStepCard } from "@/components/MacroStepCard";
|
||||||
import { DEFAULT_DELAY, MAX_STEPS_PER_MACRO, MAX_KEYS_PER_STEP } from "@/constants/macros";
|
import {
|
||||||
|
DEFAULT_DELAY,
|
||||||
|
MAX_STEPS_PER_MACRO,
|
||||||
|
MAX_KEYS_PER_STEP,
|
||||||
|
} from "@/constants/macros";
|
||||||
import FieldLabel from "@/components/FieldLabel";
|
import FieldLabel from "@/components/FieldLabel";
|
||||||
|
|
||||||
interface ValidationErrors {
|
interface ValidationErrors {
|
||||||
name?: string;
|
name?: string;
|
||||||
steps?: Record<number, {
|
steps?: Record<
|
||||||
|
number,
|
||||||
|
{
|
||||||
keys?: string;
|
keys?: string;
|
||||||
modifiers?: string;
|
modifiers?: string;
|
||||||
delay?: string;
|
delay?: string;
|
||||||
}>;
|
}
|
||||||
|
>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MacroFormProps {
|
interface MacroFormProps {
|
||||||
|
@ -57,12 +63,14 @@ export function MacroForm({
|
||||||
if (!macro.steps?.length) {
|
if (!macro.steps?.length) {
|
||||||
newErrors.steps = { 0: { keys: "At least one step is required" } };
|
newErrors.steps = { 0: { keys: "At least one step is required" } };
|
||||||
} else {
|
} else {
|
||||||
const hasKeyOrModifier = macro.steps.some(step =>
|
const hasKeyOrModifier = macro.steps.some(
|
||||||
(step.keys?.length || 0) > 0 || (step.modifiers?.length || 0) > 0
|
step => (step.keys?.length || 0) > 0 || (step.modifiers?.length || 0) > 0,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!hasKeyOrModifier) {
|
if (!hasKeyOrModifier) {
|
||||||
newErrors.steps = { 0: { keys: "At least one step must have keys or modifiers" } };
|
newErrors.steps = {
|
||||||
|
0: { keys: "At least one step must have keys or modifiers" },
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,7 +95,10 @@ export function MacroForm({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleKeySelect = (stepIndex: number, option: { value: string | null; keys?: string[] }) => {
|
const handleKeySelect = (
|
||||||
|
stepIndex: number,
|
||||||
|
option: { value: string | null; keys?: string[] },
|
||||||
|
) => {
|
||||||
const newSteps = [...(macro.steps || [])];
|
const newSteps = [...(macro.steps || [])];
|
||||||
if (!newSteps[stepIndex]) return;
|
if (!newSteps[stepIndex]) return;
|
||||||
|
|
||||||
|
@ -97,7 +108,9 @@ export function MacroForm({
|
||||||
if (!newSteps[stepIndex].keys) {
|
if (!newSteps[stepIndex].keys) {
|
||||||
newSteps[stepIndex].keys = [];
|
newSteps[stepIndex].keys = [];
|
||||||
}
|
}
|
||||||
const keysArray = Array.isArray(newSteps[stepIndex].keys) ? newSteps[stepIndex].keys : [];
|
const keysArray = Array.isArray(newSteps[stepIndex].keys)
|
||||||
|
? newSteps[stepIndex].keys
|
||||||
|
: [];
|
||||||
if (keysArray.length >= MAX_KEYS_PER_STEP) {
|
if (keysArray.length >= MAX_KEYS_PER_STEP) {
|
||||||
showTemporaryError(`Maximum of ${MAX_KEYS_PER_STEP} keys per step allowed`);
|
showTemporaryError(`Maximum of ${MAX_KEYS_PER_STEP} keys per step allowed`);
|
||||||
return;
|
return;
|
||||||
|
@ -148,9 +161,9 @@ export function MacroForm({
|
||||||
setMacro({ ...macro, steps: newSteps });
|
setMacro({ ...macro, steps: newSteps });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleStepMove = (stepIndex: number, direction: 'up' | 'down') => {
|
const handleStepMove = (stepIndex: number, direction: "up" | "down") => {
|
||||||
const newSteps = [...(macro.steps || [])];
|
const newSteps = [...(macro.steps || [])];
|
||||||
const newIndex = direction === 'up' ? stepIndex - 1 : stepIndex + 1;
|
const newIndex = direction === "up" ? stepIndex - 1 : stepIndex + 1;
|
||||||
[newSteps[stepIndex], newSteps[newIndex]] = [newSteps[newIndex], newSteps[stepIndex]];
|
[newSteps[stepIndex], newSteps[newIndex]] = [newSteps[newIndex], newSteps[stepIndex]];
|
||||||
setMacro({ ...macro, steps: newSteps });
|
setMacro({ ...macro, steps: newSteps });
|
||||||
};
|
};
|
||||||
|
@ -181,7 +194,10 @@ export function MacroForm({
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between text-sm">
|
<div className="flex items-center justify-between text-sm">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<FieldLabel label="Steps" description={`Keys/modifiers executed in sequence with a delay between each step.`} />
|
<FieldLabel
|
||||||
|
label="Steps"
|
||||||
|
description={`Keys/modifiers executed in sequence with a delay between each step.`}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-slate-500 dark:text-slate-400">
|
<span className="text-slate-500 dark:text-slate-400">
|
||||||
{macro.steps?.length || 0}/{MAX_STEPS_PER_MACRO} steps
|
{macro.steps?.length || 0}/{MAX_STEPS_PER_MACRO} steps
|
||||||
|
@ -199,18 +215,24 @@ export function MacroForm({
|
||||||
key={stepIndex}
|
key={stepIndex}
|
||||||
step={step}
|
step={step}
|
||||||
stepIndex={stepIndex}
|
stepIndex={stepIndex}
|
||||||
onDelete={macro.steps && macro.steps.length > 1 ? () => {
|
onDelete={
|
||||||
|
macro.steps && macro.steps.length > 1
|
||||||
|
? () => {
|
||||||
const newSteps = [...(macro.steps || [])];
|
const newSteps = [...(macro.steps || [])];
|
||||||
newSteps.splice(stepIndex, 1);
|
newSteps.splice(stepIndex, 1);
|
||||||
setMacro(prev => ({ ...prev, steps: newSteps }));
|
setMacro(prev => ({ ...prev, steps: newSteps }));
|
||||||
} : undefined}
|
}
|
||||||
onMoveUp={() => handleStepMove(stepIndex, 'up')}
|
: undefined
|
||||||
onMoveDown={() => handleStepMove(stepIndex, 'down')}
|
}
|
||||||
onKeySelect={(option) => handleKeySelect(stepIndex, option)}
|
onMoveUp={() => handleStepMove(stepIndex, "up")}
|
||||||
onKeyQueryChange={(query) => handleKeyQueryChange(stepIndex, query)}
|
onMoveDown={() => handleStepMove(stepIndex, "down")}
|
||||||
keyQuery={keyQueries[stepIndex] || ''}
|
onKeySelect={option => handleKeySelect(stepIndex, option)}
|
||||||
onModifierChange={(modifiers) => handleModifierChange(stepIndex, modifiers)}
|
onKeyQueryChange={query => handleKeyQueryChange(stepIndex, query)}
|
||||||
onDelayChange={(delay) => handleDelayChange(stepIndex, delay)}
|
keyQuery={keyQueries[stepIndex] || ""}
|
||||||
|
onModifierChange={modifiers =>
|
||||||
|
handleModifierChange(stepIndex, modifiers)
|
||||||
|
}
|
||||||
|
onDelayChange={delay => handleDelayChange(stepIndex, delay)}
|
||||||
isLastStep={stepIndex === (macro.steps?.length || 0) - 1}
|
isLastStep={stepIndex === (macro.steps?.length || 0) - 1}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
@ -223,10 +245,12 @@ export function MacroForm({
|
||||||
theme="light"
|
theme="light"
|
||||||
fullWidth
|
fullWidth
|
||||||
LeadingIcon={LuPlus}
|
LeadingIcon={LuPlus}
|
||||||
text={`Add Step ${isMaxStepsReached ? `(${MAX_STEPS_PER_MACRO} max)` : ''}`}
|
text={`Add Step ${isMaxStepsReached ? `(${MAX_STEPS_PER_MACRO} max)` : ""}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (isMaxStepsReached) {
|
if (isMaxStepsReached) {
|
||||||
showTemporaryError(`You can only add a maximum of ${MAX_STEPS_PER_MACRO} steps per macro.`);
|
showTemporaryError(
|
||||||
|
`You can only add a maximum of ${MAX_STEPS_PER_MACRO} steps per macro.`,
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -234,7 +258,7 @@ export function MacroForm({
|
||||||
...prev,
|
...prev,
|
||||||
steps: [
|
steps: [
|
||||||
...(prev.steps || []),
|
...(prev.steps || []),
|
||||||
{ keys: [], modifiers: [], delay: DEFAULT_DELAY }
|
{ keys: [], modifiers: [], delay: DEFAULT_DELAY },
|
||||||
],
|
],
|
||||||
}));
|
}));
|
||||||
setErrors({});
|
setErrors({});
|
||||||
|
@ -257,12 +281,7 @@ export function MacroForm({
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button size="SM" theme="light" text="Cancel" onClick={onCancel} />
|
||||||
size="SM"
|
|
||||||
theme="light"
|
|
||||||
text="Cancel"
|
|
||||||
onClick={onCancel}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -18,16 +18,18 @@ const Modal = React.memo(function Modal({
|
||||||
<Dialog open={open} onClose={onClose} className="relative z-20">
|
<Dialog open={open} onClose={onClose} className="relative z-20">
|
||||||
<DialogBackdrop
|
<DialogBackdrop
|
||||||
transition
|
transition
|
||||||
className="fixed inset-0 bg-gray-500/75 transition-opacity data-[closed]:opacity-0 data-[enter]:duration-500 data-[leave]:duration-200 data-[enter]:ease-out data-[leave]:ease-in dark:bg-slate-900/90"
|
className="fixed inset-0 bg-gray-500/75 transition-opacity data-closed:opacity-0 data-enter:duration-500 data-leave:duration-200 data-enter:ease-out data-leave:ease-in dark:bg-slate-900/90"
|
||||||
/>
|
/>
|
||||||
<div className="fixed inset-0 z-20 w-screen overflow-y-auto">
|
<div className="fixed inset-0 z-20 w-screen overflow-y-auto" style={{
|
||||||
|
scrollbarGutter: 'stable'
|
||||||
|
}}>
|
||||||
{/* TODO: This doesn't work well with other-sessions */}
|
{/* TODO: This doesn't work well with other-sessions */}
|
||||||
<div className="flex min-h-full items-end justify-center p-4 text-center md:items-baseline md:p-4">
|
<div className="flex min-h-full items-end justify-center p-4 text-center md:items-baseline md:p-4">
|
||||||
<DialogPanel
|
<DialogPanel
|
||||||
transition
|
transition
|
||||||
className={cx(
|
className={cx(
|
||||||
"pointer-events-none relative w-full md:my-8 md:!mt-[10vh]",
|
"pointer-events-none relative w-full md:my-8 md:mt-[10vh]!",
|
||||||
"transform transition-all data-[closed]:translate-y-8 data-[closed]:opacity-0 data-[enter]:duration-500 data-[leave]:duration-200 data-[enter]:ease-out data-[leave]:ease-in",
|
"transform transition-all data-closed:translate-y-8 data-closed:opacity-0 data-enter:duration-500 data-leave:duration-200 data-enter:ease-out data-leave:ease-in",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|
|
@ -63,7 +63,7 @@ export const SelectMenuBasic = React.forwardRef<HTMLSelectElement, SelectMenuPro
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{label && <FieldLabel label={label} id={id} as="span" />}
|
{label && <FieldLabel label={label} id={id} as="span" />}
|
||||||
<Card className="w-auto !border border-solid !border-slate-800/30 shadow outline-0 dark:!border-slate-300/30">
|
<Card className="w-auto border! border-solid border-slate-800/30! shadow-xs outline-0 dark:border-slate-300/30!">
|
||||||
<select
|
<select
|
||||||
ref={ref}
|
ref={ref}
|
||||||
name={name}
|
name={name}
|
||||||
|
@ -72,7 +72,7 @@ export const SelectMenuBasic = React.forwardRef<HTMLSelectElement, SelectMenuPro
|
||||||
classes,
|
classes,
|
||||||
|
|
||||||
// General styling
|
// General styling
|
||||||
"block w-full cursor-pointer rounded border-none py-0 font-medium shadow-none outline-0 transition duration-300",
|
"block w-full cursor-pointer rounded-sm border-none py-0 font-medium shadow-none outline-0 transition duration-300",
|
||||||
|
|
||||||
// Hover
|
// Hover
|
||||||
"hover:bg-blue-50/80 active:bg-blue-100/60 disabled:hover:bg-white",
|
"hover:bg-blue-50/80 active:bg-blue-100/60 disabled:hover:bg-white",
|
||||||
|
|
|
@ -23,7 +23,7 @@ const variants = cva({
|
||||||
export default function StepCounter({ nSteps, currStepIdx, size = "MD" }: Props) {
|
export default function StepCounter({ nSteps, currStepIdx, size = "MD" }: Props) {
|
||||||
const textStyle = variants({ size });
|
const textStyle = variants({ size });
|
||||||
return (
|
return (
|
||||||
<Card className="!inline-flex w-auto select-none items-center justify-center gap-x-2 rounded-lg p-1">
|
<Card className="inline-flex! w-auto select-none items-center justify-center gap-x-2 rounded-lg p-1">
|
||||||
{[...Array(nSteps).keys()].map(i => {
|
{[...Array(nSteps).keys()].map(i => {
|
||||||
if (i < currStepIdx) {
|
if (i < currStepIdx) {
|
||||||
return (
|
return (
|
||||||
|
@ -44,7 +44,7 @@ export default function StepCounter({ nSteps, currStepIdx, size = "MD" }: Props)
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cx(
|
className={cx(
|
||||||
"rounded-md border border-blue-800 bg-blue-700 px-2 py-1 font-medium text-white shadow-sm dark:border-blue-300",
|
"rounded-md border border-blue-800 bg-blue-700 px-2 py-1 font-medium text-white shadow-xs dark:border-blue-300",
|
||||||
textStyle,
|
textStyle,
|
||||||
)}
|
)}
|
||||||
key={`${i}-${currStepIdx}`}
|
key={`${i}-${currStepIdx}`}
|
||||||
|
@ -57,7 +57,7 @@ export default function StepCounter({ nSteps, currStepIdx, size = "MD" }: Props)
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
className={cx(
|
className={cx(
|
||||||
"flex items-center justify-center !rounded-full text-slate-600 dark:text-slate-400",
|
"flex items-center justify-center rounded-full! text-slate-600 dark:text-slate-400",
|
||||||
textStyle,
|
textStyle,
|
||||||
size === "SM" ? "h-5 w-5" : "h-6 w-6",
|
size === "SM" ? "h-5 w-5" : "h-6 w-6",
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -61,9 +61,9 @@ function Terminal({
|
||||||
dataChannel,
|
dataChannel,
|
||||||
type,
|
type,
|
||||||
}: {
|
}: {
|
||||||
title: string;
|
readonly title: string;
|
||||||
dataChannel: RTCDataChannel;
|
readonly dataChannel: RTCDataChannel;
|
||||||
type: AvailableTerminalTypes;
|
readonly type: AvailableTerminalTypes;
|
||||||
}) {
|
}) {
|
||||||
const enableTerminal = useUiStore(state => state.terminalType == type);
|
const enableTerminal = useUiStore(state => state.terminalType == type);
|
||||||
const setTerminalType = useUiStore(state => state.setTerminalType);
|
const setTerminalType = useUiStore(state => state.setTerminalType);
|
||||||
|
@ -171,12 +171,12 @@ function Terminal({
|
||||||
[
|
[
|
||||||
// Base styles
|
// Base styles
|
||||||
"fixed bottom-0 w-full transform transition duration-500 ease-in-out",
|
"fixed bottom-0 w-full transform transition duration-500 ease-in-out",
|
||||||
"translate-y-[0px]",
|
"-translate-y-[0px]",
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
"pointer-events-none translate-y-[500px] opacity-100 transition duration-300":
|
"pointer-events-none translate-y-[500px] opacity-100 transition duration-300":
|
||||||
!enableTerminal,
|
!enableTerminal,
|
||||||
"pointer-events-auto translate-y-[0px] opacity-100 transition duration-300":
|
"pointer-events-auto -translate-y-[0px] opacity-100 transition duration-300":
|
||||||
enableTerminal,
|
enableTerminal,
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -17,7 +17,7 @@ const TextArea = React.forwardRef<HTMLTextAreaElement, TextAreaProps>(
|
||||||
className={cx(
|
className={cx(
|
||||||
"relative w-full",
|
"relative w-full",
|
||||||
"invalid-within::ring-2 invalid-within::ring-red-600 invalid-within::ring-offset-2",
|
"invalid-within::ring-2 invalid-within::ring-red-600 invalid-within::ring-offset-2",
|
||||||
"focus-within:border-slate-300 focus-within:outline-none focus-within:ring-1 focus-within:ring-blue-700 dark:focus-within:border-slate-600",
|
"focus-within:border-slate-300 focus-within:outline-hidden focus-within:ring-1 focus-within:ring-blue-700 dark:focus-within:border-slate-600",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<textarea
|
<textarea
|
||||||
|
@ -25,7 +25,7 @@ const TextArea = React.forwardRef<HTMLTextAreaElement, TextAreaProps>(
|
||||||
{...props}
|
{...props}
|
||||||
id="asd"
|
id="asd"
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"block w-full rounded border-transparent bg-transparent text-black placeholder:text-slate-300 focus:ring-0 disabled:pointer-events-none disabled:select-none disabled:bg-slate-50 disabled:text-slate-300 dark:text-white dark:placeholder:text-slate-500 dark:disabled:bg-slate-800 sm:text-sm",
|
"block w-full rounded-sm border-transparent bg-transparent text-black placeholder:text-slate-300 focus:ring-0 disabled:pointer-events-none disabled:select-none disabled:bg-slate-50 disabled:text-slate-300 dark:text-white dark:placeholder:text-slate-500 dark:disabled:bg-slate-800 sm:text-sm",
|
||||||
props.className,
|
props.className,
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -11,7 +11,7 @@ export default function UpdateInProgressStatusCard() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full select-none opacity-100 transition-all duration-300 ease-in-out">
|
<div className="w-full select-none opacity-100 transition-all duration-300 ease-in-out">
|
||||||
<GridCard cardClassName="!shadow-xl">
|
<GridCard cardClassName="shadow-xl!">
|
||||||
<div className="flex items-center justify-between gap-x-3 px-2.5 py-2.5 text-black dark:text-white">
|
<div className="flex items-center justify-between gap-x-3 px-2.5 py-2.5 text-black dark:text-white">
|
||||||
<div className="flex items-center gap-x-3">
|
<div className="flex items-center gap-x-3">
|
||||||
<LoadingSpinner className={cx("h-5 w-5", "shrink-0 text-blue-700")} />
|
<LoadingSpinner className={cx("h-5 w-5", "shrink-0 text-blue-700")} />
|
||||||
|
|
|
@ -150,7 +150,7 @@ export function UsbDeviceSetting() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fieldset disabled={loading} className="space-y-4">
|
<Fieldset disabled={loading} className="space-y-4">
|
||||||
<div className="h-[1px] w-full bg-slate-800/10 dark:bg-slate-300/20" />
|
<div className="h-px w-full bg-slate-800/10 dark:bg-slate-300/20" />
|
||||||
|
|
||||||
<SettingsSectionHeader
|
<SettingsSectionHeader
|
||||||
title="USB Device"
|
title="USB Device"
|
||||||
|
|
|
@ -3,18 +3,18 @@ import { ExclamationTriangleIcon } from "@heroicons/react/24/solid";
|
||||||
import { ArrowPathIcon, ArrowRightIcon } from "@heroicons/react/16/solid";
|
import { ArrowPathIcon, ArrowRightIcon } from "@heroicons/react/16/solid";
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
import { LuPlay } from "react-icons/lu";
|
import { LuPlay } from "react-icons/lu";
|
||||||
|
import { BsMouseFill } from "react-icons/bs";
|
||||||
|
|
||||||
import { Button, LinkButton } from "@components/Button";
|
import { Button, LinkButton } from "@components/Button";
|
||||||
import LoadingSpinner from "@components/LoadingSpinner";
|
import LoadingSpinner from "@components/LoadingSpinner";
|
||||||
import Card, { GridCard } from "@components/Card";
|
import Card, { GridCard } from "@components/Card";
|
||||||
import { BsMouseFill } from "react-icons/bs";
|
|
||||||
|
|
||||||
interface OverlayContentProps {
|
interface OverlayContentProps {
|
||||||
children: React.ReactNode;
|
readonly children: React.ReactNode;
|
||||||
}
|
}
|
||||||
function OverlayContent({ children }: OverlayContentProps) {
|
function OverlayContent({ children }: OverlayContentProps) {
|
||||||
return (
|
return (
|
||||||
<GridCard cardClassName="h-full pointer-events-auto !outline-none">
|
<GridCard cardClassName="h-full pointer-events-auto outline-hidden!">
|
||||||
<div className="flex h-full w-full flex-col items-center justify-center rounded-md border border-slate-800/30 dark:border-slate-300/20">
|
<div className="flex h-full w-full flex-col items-center justify-center rounded-md border border-slate-800/30 dark:border-slate-300/20">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
@ -23,7 +23,7 @@ function OverlayContent({ children }: OverlayContentProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LoadingOverlayProps {
|
interface LoadingOverlayProps {
|
||||||
show: boolean;
|
readonly show: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LoadingVideoOverlay({ show }: LoadingOverlayProps) {
|
export function LoadingVideoOverlay({ show }: LoadingOverlayProps) {
|
||||||
|
@ -57,8 +57,8 @@ export function LoadingVideoOverlay({ show }: LoadingOverlayProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LoadingConnectionOverlayProps {
|
interface LoadingConnectionOverlayProps {
|
||||||
show: boolean;
|
readonly show: boolean;
|
||||||
text: string;
|
readonly text: string;
|
||||||
}
|
}
|
||||||
export function LoadingConnectionOverlay({ show, text }: LoadingConnectionOverlayProps) {
|
export function LoadingConnectionOverlay({ show, text }: LoadingConnectionOverlayProps) {
|
||||||
return (
|
return (
|
||||||
|
@ -91,8 +91,8 @@ export function LoadingConnectionOverlay({ show, text }: LoadingConnectionOverla
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ConnectionErrorOverlayProps {
|
interface ConnectionErrorOverlayProps {
|
||||||
show: boolean;
|
readonly show: boolean;
|
||||||
setupPeerConnection: () => Promise<void>;
|
readonly setupPeerConnection: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ConnectionFailedOverlay({
|
export function ConnectionFailedOverlay({
|
||||||
|
@ -153,7 +153,7 @@ export function ConnectionFailedOverlay({
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PeerConnectionDisconnectedOverlay {
|
interface PeerConnectionDisconnectedOverlay {
|
||||||
show: boolean;
|
readonly show: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PeerConnectionDisconnectedOverlay({
|
export function PeerConnectionDisconnectedOverlay({
|
||||||
|
@ -207,8 +207,8 @@ export function PeerConnectionDisconnectedOverlay({
|
||||||
}
|
}
|
||||||
|
|
||||||
interface HDMIErrorOverlayProps {
|
interface HDMIErrorOverlayProps {
|
||||||
show: boolean;
|
readonly show: boolean;
|
||||||
hdmiState: string;
|
readonly hdmiState: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function HDMIErrorOverlay({ show, hdmiState }: HDMIErrorOverlayProps) {
|
export function HDMIErrorOverlay({ show, hdmiState }: HDMIErrorOverlayProps) {
|
||||||
|
@ -242,8 +242,8 @@ export function HDMIErrorOverlay({ show, hdmiState }: HDMIErrorOverlayProps) {
|
||||||
Ensure source device is powered on and outputting a signal
|
Ensure source device is powered on and outputting a signal
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
If using an adapter, ensure it's compatible and
|
If using an adapter, ensure it's compatible and functioning
|
||||||
functioning correctly
|
correctly
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
@ -310,8 +310,8 @@ export function HDMIErrorOverlay({ show, hdmiState }: HDMIErrorOverlayProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface NoAutoplayPermissionsOverlayProps {
|
interface NoAutoplayPermissionsOverlayProps {
|
||||||
show: boolean;
|
readonly show: boolean;
|
||||||
onPlayClick: () => void;
|
readonly onPlayClick: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NoAutoplayPermissionsOverlay({
|
export function NoAutoplayPermissionsOverlay({
|
||||||
|
@ -361,7 +361,7 @@ export function NoAutoplayPermissionsOverlay({
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PointerLockBarProps {
|
interface PointerLockBarProps {
|
||||||
show: boolean;
|
readonly show: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PointerLockBar({ show }: PointerLockBarProps) {
|
export function PointerLockBar({ show }: PointerLockBarProps) {
|
||||||
|
@ -369,15 +369,15 @@ export function PointerLockBar({ show }: PointerLockBarProps) {
|
||||||
<AnimatePresence mode="wait">
|
<AnimatePresence mode="wait">
|
||||||
{show ? (
|
{show ? (
|
||||||
<motion.div
|
<motion.div
|
||||||
className="absolute -top-[36px] left-0 right-0 z-20 bg-white"
|
className="flex w-full items-center justify-between bg-transparent"
|
||||||
initial={{ y: 20, opacity: 0, zIndex: 0 }}
|
initial={{ opacity: 0, zIndex: 0 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, zIndex: 20 }}
|
||||||
exit={{ y: 43, zIndex: 0 }}
|
exit={{ opacity: 0, zIndex: 0 }}
|
||||||
transition={{ duration: 0.5, ease: "easeInOut", delay: 0.5 }}
|
transition={{ duration: 0.5, ease: "easeInOut", delay: 0.5 }}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<Card className="rounded-b-none shadow-none !outline-0">
|
<Card className="rounded-b-none shadow-none outline-0!">
|
||||||
<div className="flex items-center justify-between border border-slate-800/50 px-4 py-2 outline-0 backdrop-blur-sm dark:border-slate-300/20 dark:bg-slate-800">
|
<div className="flex items-center justify-between border border-slate-800/50 px-4 py-2 outline-0 backdrop-blur-xs dark:border-slate-300/20 dark:bg-slate-800">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<BsMouseFill className="h-4 w-4 text-blue-700 dark:text-blue-500" />
|
<BsMouseFill className="h-4 w-4 text-blue-700 dark:text-blue-500" />
|
||||||
<span className="text-sm text-black dark:text-white">
|
<span className="text-sm text-black dark:text-white">
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useShallow } from "zustand/react/shallow";
|
||||||
import Keyboard from "react-simple-keyboard";
|
|
||||||
import { ChevronDownIcon } from "@heroicons/react/16/solid";
|
import { ChevronDownIcon } from "@heroicons/react/16/solid";
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
import { AnimatePresence, motion } from "framer-motion";
|
||||||
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
import Keyboard from "react-simple-keyboard";
|
||||||
|
|
||||||
import Card from "@components/Card";
|
import Card from "@components/Card";
|
||||||
// eslint-disable-next-line import/order
|
// eslint-disable-next-line import/order
|
||||||
|
@ -9,12 +10,12 @@ import { Button } from "@components/Button";
|
||||||
|
|
||||||
import "react-simple-keyboard/build/css/index.css";
|
import "react-simple-keyboard/build/css/index.css";
|
||||||
|
|
||||||
import { useHidStore, useUiStore } from "@/hooks/stores";
|
|
||||||
import { cx } from "@/cva.config";
|
|
||||||
import { keys, modifiers, keyDisplayMap } from "@/keyboardMappings";
|
|
||||||
import useKeyboard from "@/hooks/useKeyboard";
|
|
||||||
import DetachIconRaw from "@/assets/detach-icon.svg";
|
|
||||||
import AttachIconRaw from "@/assets/attach-icon.svg";
|
import AttachIconRaw from "@/assets/attach-icon.svg";
|
||||||
|
import DetachIconRaw from "@/assets/detach-icon.svg";
|
||||||
|
import { cx } from "@/cva.config";
|
||||||
|
import { useHidStore, useSettingsStore, useUiStore } from "@/hooks/stores";
|
||||||
|
import useKeyboard from "@/hooks/useKeyboard";
|
||||||
|
import { keyDisplayMap, keys, modifiers } from "@/keyboardMappings";
|
||||||
|
|
||||||
export const DetachIcon = ({ className }: { className?: string }) => {
|
export const DetachIcon = ({ className }: { className?: string }) => {
|
||||||
return <img src={DetachIconRaw} alt="Detach Icon" className={className} />;
|
return <img src={DetachIconRaw} alt="Detach Icon" className={className} />;
|
||||||
|
@ -40,7 +41,17 @@ function KeyboardWrapper() {
|
||||||
const [isDragging, setIsDragging] = useState(false);
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
const [position, setPosition] = useState({ x: 0, y: 0 });
|
const [position, setPosition] = useState({ x: 0, y: 0 });
|
||||||
const [newPosition, setNewPosition] = useState({ x: 0, y: 0 });
|
const [newPosition, setNewPosition] = useState({ x: 0, y: 0 });
|
||||||
const isCapsLockActive = useHidStore(state => state.isCapsLockActive);
|
|
||||||
|
const isCapsLockActive = useHidStore(useShallow(state => state.keyboardLedState?.caps_lock));
|
||||||
|
|
||||||
|
// HID related states
|
||||||
|
const keyboardLedStateSyncAvailable = useHidStore(state => state.keyboardLedStateSyncAvailable);
|
||||||
|
const keyboardLedSync = useSettingsStore(state => state.keyboardLedSync);
|
||||||
|
const isKeyboardLedManagedByHost = useMemo(() =>
|
||||||
|
keyboardLedSync !== "browser" && keyboardLedStateSyncAvailable,
|
||||||
|
[keyboardLedSync, keyboardLedStateSyncAvailable],
|
||||||
|
);
|
||||||
|
|
||||||
const setIsCapsLockActive = useHidStore(state => state.setIsCapsLockActive);
|
const setIsCapsLockActive = useHidStore(state => state.setIsCapsLockActive);
|
||||||
|
|
||||||
const startDrag = useCallback((e: MouseEvent | TouchEvent) => {
|
const startDrag = useCallback((e: MouseEvent | TouchEvent) => {
|
||||||
|
@ -143,18 +154,30 @@ function KeyboardWrapper() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (key === "CtrlAltBackspace") {
|
||||||
|
sendKeyboardEvent(
|
||||||
|
[keys["Backspace"]],
|
||||||
|
[modifiers["ControlLeft"], modifiers["AltLeft"]],
|
||||||
|
);
|
||||||
|
|
||||||
|
setTimeout(resetKeyboardState, 100);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (isKeyShift || isKeyCaps) {
|
if (isKeyShift || isKeyCaps) {
|
||||||
toggleLayout();
|
toggleLayout();
|
||||||
|
|
||||||
if (isCapsLockActive) {
|
if (isCapsLockActive) {
|
||||||
|
if (!isKeyboardLedManagedByHost) {
|
||||||
setIsCapsLockActive(false);
|
setIsCapsLockActive(false);
|
||||||
|
}
|
||||||
sendKeyboardEvent([keys["CapsLock"]], []);
|
sendKeyboardEvent([keys["CapsLock"]], []);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle caps lock state change
|
// Handle caps lock state change
|
||||||
if (isKeyCaps) {
|
if (isKeyCaps && !isKeyboardLedManagedByHost) {
|
||||||
setIsCapsLockActive(!isCapsLockActive);
|
setIsCapsLockActive(!isCapsLockActive);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -173,7 +196,7 @@ function KeyboardWrapper() {
|
||||||
|
|
||||||
setTimeout(resetKeyboardState, 100);
|
setTimeout(resetKeyboardState, 100);
|
||||||
},
|
},
|
||||||
[isCapsLockActive, sendKeyboardEvent, resetKeyboardState, setIsCapsLockActive],
|
[isCapsLockActive, isKeyboardLedManagedByHost, sendKeyboardEvent, resetKeyboardState, setIsCapsLockActive],
|
||||||
);
|
);
|
||||||
|
|
||||||
const virtualKeyboard = useHidStore(state => state.isVirtualKeyboardEnabled);
|
const virtualKeyboard = useHidStore(state => state.isVirtualKeyboardEnabled);
|
||||||
|
@ -257,13 +280,13 @@ function KeyboardWrapper() {
|
||||||
buttonTheme={[
|
buttonTheme={[
|
||||||
{
|
{
|
||||||
class: "combination-key",
|
class: "combination-key",
|
||||||
buttons: "CtrlAltDelete AltMetaEscape",
|
buttons: "CtrlAltDelete AltMetaEscape CtrlAltBackspace",
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
display={keyDisplayMap}
|
display={keyDisplayMap}
|
||||||
layout={{
|
layout={{
|
||||||
default: [
|
default: [
|
||||||
"CtrlAltDelete AltMetaEscape",
|
"CtrlAltDelete AltMetaEscape CtrlAltBackspace",
|
||||||
"Escape F1 F2 F3 F4 F5 F6 F7 F8 F9 F10 F11 F12",
|
"Escape F1 F2 F3 F4 F5 F6 F7 F8 F9 F10 F11 F12",
|
||||||
"Backquote Digit1 Digit2 Digit3 Digit4 Digit5 Digit6 Digit7 Digit8 Digit9 Digit0 Minus Equal Backspace",
|
"Backquote Digit1 Digit2 Digit3 Digit4 Digit5 Digit6 Digit7 Digit8 Digit9 Digit0 Minus Equal Backspace",
|
||||||
"Tab KeyQ KeyW KeyE KeyR KeyT KeyY KeyU KeyI KeyO KeyP BracketLeft BracketRight Backslash",
|
"Tab KeyQ KeyW KeyE KeyR KeyT KeyY KeyU KeyI KeyO KeyP BracketLeft BracketRight Backslash",
|
||||||
|
@ -272,7 +295,7 @@ function KeyboardWrapper() {
|
||||||
"ControlLeft AltLeft MetaLeft Space MetaRight AltRight",
|
"ControlLeft AltLeft MetaLeft Space MetaRight AltRight",
|
||||||
],
|
],
|
||||||
shift: [
|
shift: [
|
||||||
"CtrlAltDelete AltMetaEscape",
|
"CtrlAltDelete AltMetaEscape CtrlAltBackspace",
|
||||||
"Escape F1 F2 F3 F4 F5 F6 F7 F8 F9 F10 F11 F12",
|
"Escape F1 F2 F3 F4 F5 F6 F7 F8 F9 F10 F11 F12",
|
||||||
"(Backquote) (Digit1) (Digit2) (Digit3) (Digit4) (Digit5) (Digit6) (Digit7) (Digit8) (Digit9) (Digit0) (Minus) (Equal) (Backspace)",
|
"(Backquote) (Digit1) (Digit2) (Digit3) (Digit4) (Digit5) (Digit6) (Digit7) (Digit8) (Digit9) (Digit0) (Minus) (Equal) (Backspace)",
|
||||||
"Tab (KeyQ) (KeyW) (KeyE) (KeyR) (KeyT) (KeyY) (KeyU) (KeyI) (KeyO) (KeyP) (BracketLeft) (BracketRight) (Backslash)",
|
"Tab (KeyQ) (KeyW) (KeyE) (KeyR) (KeyT) (KeyY) (KeyU) (KeyI) (KeyO) (KeyP) (BracketLeft) (BracketRight) (Backslash)",
|
||||||
|
@ -282,7 +305,7 @@ function KeyboardWrapper() {
|
||||||
],
|
],
|
||||||
}}
|
}}
|
||||||
disableButtonHold={true}
|
disableButtonHold={true}
|
||||||
mergeDisplay={true}
|
syncInstanceInputs={true}
|
||||||
debug={false}
|
debug={false}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
@ -290,34 +313,25 @@ function KeyboardWrapper() {
|
||||||
<Keyboard
|
<Keyboard
|
||||||
baseClass="simple-keyboard-control"
|
baseClass="simple-keyboard-control"
|
||||||
theme="simple-keyboard hg-theme-default hg-layout-default"
|
theme="simple-keyboard hg-theme-default hg-layout-default"
|
||||||
|
layoutName={layoutName}
|
||||||
|
onKeyPress={onKeyDown}
|
||||||
|
display={keyDisplayMap}
|
||||||
layout={{
|
layout={{
|
||||||
default: ["Home Pageup", "Delete End Pagedown"],
|
default: ["PrintScreen ScrollLock Pause", "Insert Home Pageup", "Delete End Pagedown"],
|
||||||
}}
|
shift: ["(PrintScreen) ScrollLock (Pause)", "Insert Home Pageup", "Delete End Pagedown"],
|
||||||
display={{
|
|
||||||
Home: "home",
|
|
||||||
Pageup: "pageup",
|
|
||||||
Delete: "delete",
|
|
||||||
End: "end",
|
|
||||||
Pagedown: "pagedown",
|
|
||||||
}}
|
}}
|
||||||
syncInstanceInputs={true}
|
syncInstanceInputs={true}
|
||||||
onKeyPress={onKeyDown}
|
|
||||||
mergeDisplay={true}
|
|
||||||
debug={false}
|
debug={false}
|
||||||
/>
|
/>
|
||||||
<Keyboard
|
<Keyboard
|
||||||
baseClass="simple-keyboard-arrows"
|
baseClass="simple-keyboard-arrows"
|
||||||
theme="simple-keyboard hg-theme-default hg-layout-default"
|
theme="simple-keyboard hg-theme-default hg-layout-default"
|
||||||
display={{
|
onKeyPress={onKeyDown}
|
||||||
ArrowLeft: "←",
|
display={keyDisplayMap}
|
||||||
ArrowRight: "→",
|
|
||||||
ArrowUp: "↑",
|
|
||||||
ArrowDown: "↓",
|
|
||||||
}}
|
|
||||||
layout={{
|
layout={{
|
||||||
default: ["ArrowUp", "ArrowLeft ArrowDown ArrowRight"],
|
default: ["ArrowUp", "ArrowLeft ArrowDown ArrowRight"],
|
||||||
}}
|
}}
|
||||||
onKeyPress={onKeyDown}
|
syncInstanceInputs={true}
|
||||||
debug={false}
|
debug={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,24 +1,22 @@
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
|
||||||
import {
|
|
||||||
useDeviceSettingsStore,
|
|
||||||
useHidStore,
|
|
||||||
useMouseStore,
|
|
||||||
useRTCStore,
|
|
||||||
useSettingsStore,
|
|
||||||
useUiStore,
|
|
||||||
useVideoStore,
|
|
||||||
} from "@/hooks/stores";
|
|
||||||
import { keys, modifiers } from "@/keyboardMappings";
|
|
||||||
import { useResizeObserver } from "usehooks-ts";
|
import { useResizeObserver } from "usehooks-ts";
|
||||||
import { cx } from "@/cva.config";
|
|
||||||
import VirtualKeyboard from "@components/VirtualKeyboard";
|
import VirtualKeyboard from "@components/VirtualKeyboard";
|
||||||
import Actionbar from "@components/ActionBar";
|
import Actionbar from "@components/ActionBar";
|
||||||
import MacroBar from "@/components/MacroBar";
|
import MacroBar from "@/components/MacroBar";
|
||||||
import InfoBar from "@components/InfoBar";
|
import InfoBar from "@components/InfoBar";
|
||||||
|
import notifications from "@/notifications";
|
||||||
import useKeyboard from "@/hooks/useKeyboard";
|
import useKeyboard from "@/hooks/useKeyboard";
|
||||||
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||||
import notifications from "@/notifications";
|
import { cx } from "@/cva.config";
|
||||||
|
import { keys, modifiers } from "@/keyboardMappings";
|
||||||
|
import {
|
||||||
|
useHidStore,
|
||||||
|
useMouseStore,
|
||||||
|
useRTCStore,
|
||||||
|
useSettingsStore,
|
||||||
|
useVideoStore,
|
||||||
|
} from "@/hooks/stores";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
HDMIErrorOverlay,
|
HDMIErrorOverlay,
|
||||||
|
@ -48,6 +46,23 @@ export default function WebRTCVideo() {
|
||||||
clientHeight: videoClientHeight,
|
clientHeight: videoClientHeight,
|
||||||
} = useVideoStore();
|
} = useVideoStore();
|
||||||
|
|
||||||
|
// Video enhancement settings
|
||||||
|
const videoSaturation = useSettingsStore(state => state.videoSaturation);
|
||||||
|
const videoBrightness = useSettingsStore(state => state.videoBrightness);
|
||||||
|
const videoContrast = useSettingsStore(state => state.videoContrast);
|
||||||
|
|
||||||
|
// HID related states
|
||||||
|
const keyboardLedStateSyncAvailable = useHidStore(state => state.keyboardLedStateSyncAvailable);
|
||||||
|
const keyboardLedSync = useSettingsStore(state => state.keyboardLedSync);
|
||||||
|
const isKeyboardLedManagedByHost = useMemo(() =>
|
||||||
|
keyboardLedSync !== "browser" && keyboardLedStateSyncAvailable,
|
||||||
|
[keyboardLedSync, keyboardLedStateSyncAvailable],
|
||||||
|
);
|
||||||
|
|
||||||
|
const setIsNumLockActive = useHidStore(state => state.setIsNumLockActive);
|
||||||
|
const setIsCapsLockActive = useHidStore(state => state.setIsCapsLockActive);
|
||||||
|
const setIsScrollLockActive = useHidStore(state => state.setIsScrollLockActive);
|
||||||
|
|
||||||
// RTC related states
|
// RTC related states
|
||||||
const peerConnection = useRTCStore(state => state.peerConnection);
|
const peerConnection = useRTCStore(state => state.peerConnection);
|
||||||
|
|
||||||
|
@ -56,13 +71,9 @@ export default function WebRTCVideo() {
|
||||||
const hdmiError = ["no_lock", "no_signal", "out_of_range"].includes(hdmiState);
|
const hdmiError = ["no_lock", "no_signal", "out_of_range"].includes(hdmiState);
|
||||||
const isVideoLoading = !isPlaying;
|
const isVideoLoading = !isPlaying;
|
||||||
|
|
||||||
// Keyboard related states
|
const [blockWheelEvent, setBlockWheelEvent] = useState(false);
|
||||||
const { setIsNumLockActive, setIsCapsLockActive, setIsScrollLockActive } =
|
|
||||||
useHidStore();
|
|
||||||
|
|
||||||
// Misc states and hooks
|
// Misc states and hooks
|
||||||
const [blockWheelEvent, setBlockWheelEvent] = useState(false);
|
|
||||||
const disableVideoFocusTrap = useUiStore(state => state.disableVideoFocusTrap);
|
|
||||||
const [send] = useJsonRpc();
|
const [send] = useJsonRpc();
|
||||||
|
|
||||||
// Video-related
|
// Video-related
|
||||||
|
@ -100,32 +111,77 @@ export default function WebRTCVideo() {
|
||||||
);
|
);
|
||||||
|
|
||||||
// Pointer lock and keyboard lock related
|
// Pointer lock and keyboard lock related
|
||||||
const isPointerLockPossible = window.location.protocol === "https:";
|
const isPointerLockPossible = window.location.protocol === "https:" || window.location.hostname === "localhost";
|
||||||
|
const isFullscreenEnabled = document.fullscreenEnabled;
|
||||||
|
|
||||||
const checkNavigatorPermissions = useCallback(async (permissionName: string) => {
|
const checkNavigatorPermissions = useCallback(async (permissionName: string) => {
|
||||||
|
if (!navigator.permissions || !navigator.permissions.query) {
|
||||||
|
return false; // if can't query permissions, assume NOT granted
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
const name = permissionName as PermissionName;
|
const name = permissionName as PermissionName;
|
||||||
const { state } = await navigator.permissions.query({ name });
|
const { state } = await navigator.permissions.query({ name });
|
||||||
return state === "granted";
|
return state === "granted";
|
||||||
|
} catch {
|
||||||
|
// ignore errors
|
||||||
|
}
|
||||||
|
return false; // if query fails, assume NOT granted
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const requestPointerLock = useCallback(async () => {
|
const requestPointerLock = useCallback(async () => {
|
||||||
if (document.pointerLockElement) return;
|
if (!isPointerLockPossible
|
||||||
|
|| videoElm.current === null
|
||||||
|
|| document.pointerLockElement) return;
|
||||||
|
|
||||||
const isPointerLockGranted = await checkNavigatorPermissions("pointer-lock");
|
const isPointerLockGranted = await checkNavigatorPermissions("pointer-lock");
|
||||||
|
|
||||||
if (isPointerLockGranted && settings.mouseMode === "relative") {
|
if (isPointerLockGranted && settings.mouseMode === "relative") {
|
||||||
videoElm.current?.requestPointerLock();
|
try {
|
||||||
|
await videoElm.current.requestPointerLock();
|
||||||
|
} catch {
|
||||||
|
// ignore errors
|
||||||
}
|
}
|
||||||
}, [checkNavigatorPermissions, settings.mouseMode]);
|
}
|
||||||
|
}, [checkNavigatorPermissions, isPointerLockPossible, settings.mouseMode]);
|
||||||
|
|
||||||
|
const requestKeyboardLock = useCallback(async () => {
|
||||||
|
if (videoElm.current === null) return;
|
||||||
|
|
||||||
|
const isKeyboardLockGranted = await checkNavigatorPermissions("keyboard-lock");
|
||||||
|
|
||||||
|
if (isKeyboardLockGranted && "keyboard" in navigator) {
|
||||||
|
try {
|
||||||
|
// @ts-expect-error - keyboard lock is not supported in all browsers
|
||||||
|
await navigator.keyboard.lock();
|
||||||
|
} catch {
|
||||||
|
// ignore errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [checkNavigatorPermissions]);
|
||||||
|
|
||||||
|
const releaseKeyboardLock = useCallback(async () => {
|
||||||
|
if (videoElm.current === null || document.fullscreenElement !== videoElm.current) return;
|
||||||
|
|
||||||
|
if ("keyboard" in navigator) {
|
||||||
|
try {
|
||||||
|
// @ts-expect-error - keyboard unlock is not supported in all browsers
|
||||||
|
await navigator.keyboard.unlock();
|
||||||
|
} catch {
|
||||||
|
// ignore errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isPointerLockPossible || !videoElm.current) return;
|
if (!isPointerLockPossible || !videoElm.current) return;
|
||||||
|
|
||||||
const handlePointerLockChange = () => {
|
const handlePointerLockChange = () => {
|
||||||
if (document.pointerLockElement) {
|
if (document.pointerLockElement) {
|
||||||
notifications.success("Pointer lock Enabled, hold escape to exit");
|
notifications.success("Pointer lock Enabled, press escape to unlock");
|
||||||
setIsPointerLockActive(true);
|
setIsPointerLockActive(true);
|
||||||
} else {
|
} else {
|
||||||
notifications.success("Pointer lock disabled");
|
notifications.success("Pointer lock Disabled");
|
||||||
setIsPointerLockActive(false);
|
setIsPointerLockActive(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -138,27 +194,39 @@ export default function WebRTCVideo() {
|
||||||
return () => {
|
return () => {
|
||||||
abortController.abort();
|
abortController.abort();
|
||||||
};
|
};
|
||||||
}, [isPointerLockPossible, videoElm]);
|
}, [isPointerLockPossible]);
|
||||||
|
|
||||||
const requestFullscreen = useCallback(async () => {
|
const requestFullscreen = useCallback(async () => {
|
||||||
videoElm.current?.requestFullscreen({
|
if (!isFullscreenEnabled || !videoElm.current) return;
|
||||||
navigationUI: "show",
|
|
||||||
});
|
|
||||||
|
|
||||||
// we do not care about pointer lock if it's for fullscreen
|
// per https://wicg.github.io/keyboard-lock/#system-key-press-handler
|
||||||
|
// If keyboard lock is activated after fullscreen is already in effect, then the user my
|
||||||
|
// see multiple messages about how to exit fullscreen. For this reason, we recommend that
|
||||||
|
// developers call lock() before they enter fullscreen:
|
||||||
|
await requestKeyboardLock();
|
||||||
await requestPointerLock();
|
await requestPointerLock();
|
||||||
|
|
||||||
const isKeyboardLockGranted = await checkNavigatorPermissions("keyboard-lock");
|
await videoElm.current.requestFullscreen({
|
||||||
if (isKeyboardLockGranted) {
|
navigationUI: "show",
|
||||||
if ("keyboard" in navigator) {
|
});
|
||||||
// @ts-ignore
|
}, [isFullscreenEnabled, requestKeyboardLock, requestPointerLock]);
|
||||||
await navigator.keyboard.lock();
|
|
||||||
|
// setup to release the keyboard lock anytime the fullscreen ends
|
||||||
|
useEffect(() => {
|
||||||
|
if (!videoElm.current) return;
|
||||||
|
|
||||||
|
const handleFullscreenChange = () => {
|
||||||
|
if (!document.fullscreenElement) {
|
||||||
|
releaseKeyboardLock();
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
}, [requestPointerLock, checkNavigatorPermissions]);
|
|
||||||
|
document.addEventListener("fullscreenchange ", handleFullscreenChange);
|
||||||
|
}, [releaseKeyboardLock]);
|
||||||
|
|
||||||
// Mouse-related
|
// Mouse-related
|
||||||
const calcDelta = (pos: number) => (Math.abs(pos) < 10 ? pos * 2 : pos);
|
const calcDelta = (pos: number) => (Math.abs(pos) < 10 ? pos * 2 : pos);
|
||||||
|
|
||||||
const sendRelMouseMovement = useCallback(
|
const sendRelMouseMovement = useCallback(
|
||||||
(x: number, y: number, buttons: number) => {
|
(x: number, y: number, buttons: number) => {
|
||||||
if (settings.mouseMode !== "relative") return;
|
if (settings.mouseMode !== "relative") return;
|
||||||
|
@ -173,18 +241,13 @@ export default function WebRTCVideo() {
|
||||||
const relMouseMoveHandler = useCallback(
|
const relMouseMoveHandler = useCallback(
|
||||||
(e: MouseEvent) => {
|
(e: MouseEvent) => {
|
||||||
if (settings.mouseMode !== "relative") return;
|
if (settings.mouseMode !== "relative") return;
|
||||||
if (isPointerLockActive === false && isPointerLockPossible === true) return;
|
if (isPointerLockActive === false && isPointerLockPossible) return;
|
||||||
|
|
||||||
// Send mouse movement
|
// Send mouse movement
|
||||||
const { buttons } = e;
|
const { buttons } = e;
|
||||||
sendRelMouseMovement(e.movementX, e.movementY, buttons);
|
sendRelMouseMovement(e.movementX, e.movementY, buttons);
|
||||||
},
|
},
|
||||||
[
|
[isPointerLockActive, isPointerLockPossible, sendRelMouseMovement, settings.mouseMode],
|
||||||
isPointerLockActive,
|
|
||||||
isPointerLockPossible,
|
|
||||||
sendRelMouseMovement,
|
|
||||||
settings.mouseMode,
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const sendAbsMouseMovement = useCallback(
|
const sendAbsMouseMovement = useCallback(
|
||||||
|
@ -238,61 +301,43 @@ export default function WebRTCVideo() {
|
||||||
const { buttons } = e;
|
const { buttons } = e;
|
||||||
sendAbsMouseMovement(x, y, buttons);
|
sendAbsMouseMovement(x, y, buttons);
|
||||||
},
|
},
|
||||||
[
|
[settings.mouseMode, videoClientWidth, videoClientHeight, videoWidth, videoHeight, sendAbsMouseMovement],
|
||||||
sendAbsMouseMovement,
|
|
||||||
videoClientHeight,
|
|
||||||
videoClientWidth,
|
|
||||||
videoWidth,
|
|
||||||
videoHeight,
|
|
||||||
settings.mouseMode,
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
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(
|
const mouseWheelHandler = useCallback(
|
||||||
(e: WheelEvent) => {
|
(e: WheelEvent) => {
|
||||||
if (blockWheelEvent) return;
|
|
||||||
|
|
||||||
// Determine if the wheel event is from a trackpad or a mouse wheel
|
if (settings.scrollThrottling && blockWheelEvent) {
|
||||||
const isTrackpad = Math.abs(e.deltaY) < trackpadThreshold;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Apply appropriate sensitivity based on input device
|
// Determine if the wheel event is an accel scroll value
|
||||||
const scrollSensitivity = isTrackpad ? trackpadSensitivity : mouseSensitivity;
|
const isAccel = Math.abs(e.deltaY) >= 100;
|
||||||
|
|
||||||
// Calculate the scroll value
|
// Calculate the accel scroll value
|
||||||
const scroll = e.deltaY * scrollSensitivity;
|
const accelScrollValue = e.deltaY / 100;
|
||||||
|
|
||||||
// Apply clamping
|
// Calculate the no accel scroll value
|
||||||
const clampedScroll = Math.max(clampMin, Math.min(clampMax, scroll));
|
const noAccelScrollValue = Math.sign(e.deltaY);
|
||||||
|
|
||||||
// Round to the nearest integer
|
// Get scroll value
|
||||||
const roundedScroll = Math.round(clampedScroll);
|
const scrollValue = isAccel ? accelScrollValue : noAccelScrollValue;
|
||||||
|
|
||||||
// Invert the scroll value to match expected behavior
|
// Apply clamping (i.e. min and max mouse wheel hardware value)
|
||||||
const invertedScroll = -roundedScroll;
|
const clampedScrollValue = Math.max(-127, Math.min(127, scrollValue));
|
||||||
|
|
||||||
send("wheelReport", { wheelY: invertedScroll });
|
// Invert the clamped scroll value to match expected behavior
|
||||||
|
const invertedScrollValue = -clampedScrollValue;
|
||||||
|
|
||||||
// Apply blocking delay
|
send("wheelReport", { wheelY: invertedScrollValue });
|
||||||
|
|
||||||
|
// Apply blocking delay based of throttling settings
|
||||||
|
if (settings.scrollThrottling && !blockWheelEvent) {
|
||||||
setBlockWheelEvent(true);
|
setBlockWheelEvent(true);
|
||||||
setTimeout(() => setBlockWheelEvent(false), blockDelay);
|
setTimeout(() => setBlockWheelEvent(false), settings.scrollThrottling);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[
|
[send, blockWheelEvent, settings],
|
||||||
blockDelay,
|
|
||||||
blockWheelEvent,
|
|
||||||
clampMax,
|
|
||||||
clampMin,
|
|
||||||
mouseSensitivity,
|
|
||||||
send,
|
|
||||||
trackpadSensitivity,
|
|
||||||
trackpadThreshold,
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const resetMousePosition = useCallback(() => {
|
const resetMousePosition = useCallback(() => {
|
||||||
|
@ -351,11 +396,7 @@ export default function WebRTCVideo() {
|
||||||
// which means the Alt Gr key state would then be "stuck". At this
|
// 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
|
// point, we would need to rely on the user to press Alt Gr again
|
||||||
// to properly release the state of that modifier.
|
// to properly release the state of that modifier.
|
||||||
.filter(
|
.filter(modifier => altKey || modifier !== modifiers["AltLeft"])
|
||||||
modifier =>
|
|
||||||
altKey ||
|
|
||||||
(modifier !== modifiers["AltLeft"]),
|
|
||||||
)
|
|
||||||
// Meta: Keep if Meta is pressed or if the key isn't a Meta key
|
// Meta: Keep if Meta is pressed or if the key isn't a Meta key
|
||||||
// Example: If metaKey is true, keep all modifiers
|
// Example: If metaKey is true, keep all modifiers
|
||||||
// If metaKey is false, filter out 0x08 (MetaLeft) and 0x80 (MetaRight)
|
// If metaKey is false, filter out 0x08 (MetaLeft) and 0x80 (MetaRight)
|
||||||
|
@ -376,16 +417,11 @@ export default function WebRTCVideo() {
|
||||||
let code = e.code;
|
let code = e.code;
|
||||||
const key = e.key;
|
const key = e.key;
|
||||||
|
|
||||||
// if (document.activeElement?.id !== "videoFocusTrap") {
|
if (!isKeyboardLedManagedByHost) {
|
||||||
// console.log("KEYUP: Not focusing on the video", document.activeElement);
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// console.log(document.activeElement);
|
|
||||||
|
|
||||||
setIsNumLockActive(e.getModifierState("NumLock"));
|
setIsNumLockActive(e.getModifierState("NumLock"));
|
||||||
setIsCapsLockActive(e.getModifierState("CapsLock"));
|
setIsCapsLockActive(e.getModifierState("CapsLock"));
|
||||||
setIsScrollLockActive(e.getModifierState("ScrollLock"));
|
setIsScrollLockActive(e.getModifierState("ScrollLock"));
|
||||||
|
}
|
||||||
|
|
||||||
if (code == "IntlBackslash" && ["`", "~"].includes(key)) {
|
if (code == "IntlBackslash" && ["`", "~"].includes(key)) {
|
||||||
code = "Backquote";
|
code = "Backquote";
|
||||||
|
@ -416,11 +452,12 @@ export default function WebRTCVideo() {
|
||||||
sendKeyboardEvent([...new Set(newKeys)], [...new Set(newModifiers)]);
|
sendKeyboardEvent([...new Set(newKeys)], [...new Set(newModifiers)]);
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
|
handleModifierKeys,
|
||||||
|
sendKeyboardEvent,
|
||||||
|
isKeyboardLedManagedByHost,
|
||||||
setIsNumLockActive,
|
setIsNumLockActive,
|
||||||
setIsCapsLockActive,
|
setIsCapsLockActive,
|
||||||
setIsScrollLockActive,
|
setIsScrollLockActive,
|
||||||
handleModifierKeys,
|
|
||||||
sendKeyboardEvent,
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -429,9 +466,11 @@ export default function WebRTCVideo() {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const prev = useHidStore.getState();
|
const prev = useHidStore.getState();
|
||||||
|
|
||||||
|
if (!isKeyboardLedManagedByHost) {
|
||||||
setIsNumLockActive(e.getModifierState("NumLock"));
|
setIsNumLockActive(e.getModifierState("NumLock"));
|
||||||
setIsCapsLockActive(e.getModifierState("CapsLock"));
|
setIsCapsLockActive(e.getModifierState("CapsLock"));
|
||||||
setIsScrollLockActive(e.getModifierState("ScrollLock"));
|
setIsScrollLockActive(e.getModifierState("ScrollLock"));
|
||||||
|
}
|
||||||
|
|
||||||
// Filtering out the key that was just released (keys[e.code])
|
// Filtering out the key that was just released (keys[e.code])
|
||||||
const newKeys = prev.activeKeys.filter(k => k !== keys[e.code]).filter(Boolean);
|
const newKeys = prev.activeKeys.filter(k => k !== keys[e.code]).filter(Boolean);
|
||||||
|
@ -445,22 +484,25 @@ export default function WebRTCVideo() {
|
||||||
sendKeyboardEvent([...new Set(newKeys)], [...new Set(newModifiers)]);
|
sendKeyboardEvent([...new Set(newKeys)], [...new Set(newModifiers)]);
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
|
handleModifierKeys,
|
||||||
|
sendKeyboardEvent,
|
||||||
|
isKeyboardLedManagedByHost,
|
||||||
setIsNumLockActive,
|
setIsNumLockActive,
|
||||||
setIsCapsLockActive,
|
setIsCapsLockActive,
|
||||||
setIsScrollLockActive,
|
setIsScrollLockActive,
|
||||||
handleModifierKeys,
|
|
||||||
sendKeyboardEvent,
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const videoKeyUpHandler = useCallback((e: KeyboardEvent) => {
|
const videoKeyUpHandler = useCallback((e: KeyboardEvent) => {
|
||||||
|
if (!videoElm.current) return;
|
||||||
|
|
||||||
// In fullscreen mode in chrome & safari, the space key is used to pause/play the video
|
// In fullscreen mode in chrome & safari, the space key is used to pause/play the video
|
||||||
// there is no way to prevent this, so we need to simply force play the video when it's paused.
|
// there is no way to prevent this, so we need to simply force play the video when it's paused.
|
||||||
// Fix only works in chrome based browsers.
|
// Fix only works in chrome based browsers.
|
||||||
if (e.code === "Space") {
|
if (e.code === "Space") {
|
||||||
if (videoElm.current?.paused == true) {
|
if (videoElm.current.paused) {
|
||||||
console.log("Force playing video");
|
console.log("Force playing video");
|
||||||
videoElm.current?.play();
|
videoElm.current.play();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
@ -469,7 +511,6 @@ export default function WebRTCVideo() {
|
||||||
(mediaStream: MediaStream) => {
|
(mediaStream: MediaStream) => {
|
||||||
if (!videoElm.current) return;
|
if (!videoElm.current) return;
|
||||||
const videoElmRefValue = videoElm.current;
|
const videoElmRefValue = videoElm.current;
|
||||||
// console.log("Adding stream to video element", videoElmRefValue);
|
|
||||||
videoElmRefValue.srcObject = mediaStream;
|
videoElmRefValue.srcObject = mediaStream;
|
||||||
updateVideoSizeStore(videoElmRefValue);
|
updateVideoSizeStore(videoElmRefValue);
|
||||||
},
|
},
|
||||||
|
@ -485,7 +526,6 @@ export default function WebRTCVideo() {
|
||||||
peerConnection.addEventListener(
|
peerConnection.addEventListener(
|
||||||
"track",
|
"track",
|
||||||
(e: RTCTrackEvent) => {
|
(e: RTCTrackEvent) => {
|
||||||
// console.log("Adding stream to video element");
|
|
||||||
addStreamToVideoElm(e.streams[0]);
|
addStreamToVideoElm(e.streams[0]);
|
||||||
},
|
},
|
||||||
{ signal },
|
{ signal },
|
||||||
|
@ -501,7 +541,6 @@ export default function WebRTCVideo() {
|
||||||
useEffect(
|
useEffect(
|
||||||
function updateVideoStream() {
|
function updateVideoStream() {
|
||||||
if (!mediaStream) return;
|
if (!mediaStream) return;
|
||||||
console.log("Updating video stream from mediaStream");
|
|
||||||
// We set the as early as possible
|
// We set the as early as possible
|
||||||
addStreamToVideoElm(mediaStream);
|
addStreamToVideoElm(mediaStream);
|
||||||
},
|
},
|
||||||
|
@ -523,9 +562,6 @@ export default function WebRTCVideo() {
|
||||||
document.addEventListener("keydown", keyDownHandler, { signal });
|
document.addEventListener("keydown", keyDownHandler, { signal });
|
||||||
document.addEventListener("keyup", keyUpHandler, { signal });
|
document.addEventListener("keyup", keyUpHandler, { signal });
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-expect-error
|
|
||||||
window.clearKeys = () => sendKeyboardEvent([], []);
|
|
||||||
window.addEventListener("blur", resetKeyboardState, { signal });
|
window.addEventListener("blur", resetKeyboardState, { signal });
|
||||||
document.addEventListener("visibilitychange", resetKeyboardState, { signal });
|
document.addEventListener("visibilitychange", resetKeyboardState, { signal });
|
||||||
|
|
||||||
|
@ -533,7 +569,7 @@ export default function WebRTCVideo() {
|
||||||
abortController.abort();
|
abortController.abort();
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[keyDownHandler, keyUpHandler, resetKeyboardState, sendKeyboardEvent],
|
[keyDownHandler, keyUpHandler, resetKeyboardState],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Setup Video Event Listeners
|
// Setup Video Event Listeners
|
||||||
|
@ -555,78 +591,41 @@ export default function WebRTCVideo() {
|
||||||
abortController.abort();
|
abortController.abort();
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[
|
[onVideoPlaying, videoKeyUpHandler],
|
||||||
absMouseMoveHandler,
|
|
||||||
resetMousePosition,
|
|
||||||
onVideoPlaying,
|
|
||||||
mouseWheelHandler,
|
|
||||||
videoKeyUpHandler,
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Setup Absolute Mouse Events
|
// Setup Mouse Events
|
||||||
useEffect(
|
useEffect(
|
||||||
function setAbsoluteMouseModeEventListeners() {
|
function setMouseModeEventListeners() {
|
||||||
const videoElmRefValue = videoElm.current;
|
const videoElmRefValue = videoElm.current;
|
||||||
if (!videoElmRefValue) return;
|
if (!videoElmRefValue) return;
|
||||||
|
const isRelativeMouseMode = (settings.mouseMode === "relative");
|
||||||
if (settings.mouseMode !== "absolute") return;
|
|
||||||
|
|
||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
const signal = abortController.signal;
|
const signal = abortController.signal;
|
||||||
|
|
||||||
videoElmRefValue.addEventListener("mousemove", absMouseMoveHandler, { signal });
|
videoElmRefValue.addEventListener("mousemove", isRelativeMouseMode ? relMouseMoveHandler : absMouseMoveHandler, { signal });
|
||||||
videoElmRefValue.addEventListener("pointerdown", absMouseMoveHandler, { signal });
|
videoElmRefValue.addEventListener("pointerdown", isRelativeMouseMode ? relMouseMoveHandler : absMouseMoveHandler, { signal });
|
||||||
videoElmRefValue.addEventListener("pointerup", absMouseMoveHandler, { signal });
|
videoElmRefValue.addEventListener("pointerup", isRelativeMouseMode ? relMouseMoveHandler :absMouseMoveHandler, { signal });
|
||||||
videoElmRefValue.addEventListener("wheel", mouseWheelHandler, {
|
videoElmRefValue.addEventListener("wheel", mouseWheelHandler, {
|
||||||
signal,
|
signal,
|
||||||
passive: true,
|
passive: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Reset the mouse position when the window is blurred or the document is hidden
|
if (isRelativeMouseMode) {
|
||||||
const local = resetMousePosition;
|
videoElmRefValue.addEventListener("click",
|
||||||
window.addEventListener("blur", local, { signal });
|
|
||||||
document.addEventListener("visibilitychange", local, { signal });
|
|
||||||
const preventContextMenu = (e: MouseEvent) => e.preventDefault();
|
|
||||||
videoElmRefValue.addEventListener("contextmenu", preventContextMenu, { signal });
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
abortController.abort();
|
|
||||||
};
|
|
||||||
},
|
|
||||||
[absMouseMoveHandler, mouseWheelHandler, resetMousePosition, settings.mouseMode],
|
|
||||||
);
|
|
||||||
|
|
||||||
// Setup Relative Mouse Events
|
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
useEffect(
|
|
||||||
function setupRelativeMouseEventListeners() {
|
|
||||||
if (settings.mouseMode !== "relative") return;
|
|
||||||
// Relative mouse mode should only be active if the pointer lock is active and Pointer Lock is possible
|
|
||||||
|
|
||||||
const videoElmRefValue = videoElm.current;
|
|
||||||
if (!videoElmRefValue) return;
|
|
||||||
|
|
||||||
const abortController = new AbortController();
|
|
||||||
const signal = abortController.signal;
|
|
||||||
|
|
||||||
videoElmRefValue.addEventListener("mousemove", relMouseMoveHandler, { signal });
|
|
||||||
videoElmRefValue.addEventListener("pointerdown", relMouseMoveHandler, { signal });
|
|
||||||
videoElmRefValue.addEventListener("pointerup", relMouseMoveHandler, { signal });
|
|
||||||
videoElmRefValue.addEventListener(
|
|
||||||
"click",
|
|
||||||
() => {
|
() => {
|
||||||
if (isPointerLockPossible && !document.pointerLockElement) {
|
if (isPointerLockPossible && !isPointerLockActive && !document.pointerLockElement) {
|
||||||
requestPointerLock();
|
requestPointerLock();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ signal },
|
{ signal },
|
||||||
);
|
);
|
||||||
videoElmRefValue.addEventListener("wheel", mouseWheelHandler, {
|
} else {
|
||||||
signal,
|
// Reset the mouse position when the window is blurred or the document is hidden
|
||||||
passive: true,
|
window.addEventListener("blur", resetMousePosition, { signal });
|
||||||
});
|
document.addEventListener("visibilitychange", resetMousePosition, { signal });
|
||||||
|
}
|
||||||
|
|
||||||
const preventContextMenu = (e: MouseEvent) => e.preventDefault();
|
const preventContextMenu = (e: MouseEvent) => e.preventDefault();
|
||||||
videoElmRefValue.addEventListener("contextmenu", preventContextMenu, { signal });
|
videoElmRefValue.addEventListener("contextmenu", preventContextMenu, { signal });
|
||||||
|
@ -635,24 +634,18 @@ export default function WebRTCVideo() {
|
||||||
abortController.abort();
|
abortController.abort();
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[
|
[absMouseMoveHandler, isPointerLockActive, isPointerLockPossible, mouseWheelHandler, relMouseMoveHandler, requestPointerLock, resetMousePosition, settings.mouseMode],
|
||||||
settings.mouseMode,
|
|
||||||
relMouseMoveHandler,
|
|
||||||
mouseWheelHandler,
|
|
||||||
disableVideoFocusTrap,
|
|
||||||
requestPointerLock,
|
|
||||||
isPointerLockPossible,
|
|
||||||
isPointerLockActive,
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const hasNoAutoPlayPermissions = useMemo(() => {
|
const hasNoAutoPlayPermissions = useMemo(() => {
|
||||||
if (peerConnection?.connectionState !== "connected") return false;
|
if (peerConnection?.connectionState !== "connected") return false;
|
||||||
if (isPlaying) return false;
|
if (isPlaying) return false;
|
||||||
if (hdmiError) return false;
|
if (hdmiError) return false;
|
||||||
if (videoHeight === 0 || videoWidth === 0) return false;
|
if (videoHeight === 0 || videoWidth === 0) return false;
|
||||||
return true;
|
return true;
|
||||||
}, [peerConnection?.connectionState, isPlaying, hdmiError, videoHeight, videoWidth]);
|
}, [hdmiError, isPlaying, peerConnection?.connectionState, videoHeight, videoWidth]);
|
||||||
|
|
||||||
const showPointerLockBar = useMemo(() => {
|
const showPointerLockBar = useMemo(() => {
|
||||||
if (settings.mouseMode !== "relative") return false;
|
if (settings.mouseMode !== "relative") return false;
|
||||||
|
@ -662,18 +655,10 @@ export default function WebRTCVideo() {
|
||||||
if (!isPlaying) return false;
|
if (!isPlaying) return false;
|
||||||
if (videoHeight === 0 || videoWidth === 0) return false;
|
if (videoHeight === 0 || videoWidth === 0) return false;
|
||||||
return true;
|
return true;
|
||||||
}, [
|
}, [isPlaying, isPointerLockActive, isPointerLockPossible, isVideoLoading, settings.mouseMode, videoHeight, videoWidth]);
|
||||||
settings.mouseMode,
|
|
||||||
isPointerLockPossible,
|
|
||||||
isPointerLockActive,
|
|
||||||
isVideoLoading,
|
|
||||||
isPlaying,
|
|
||||||
videoHeight,
|
|
||||||
videoWidth,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid h-full w-full grid-rows-layout">
|
<div className="grid h-full w-full grid-rows-(--grid-layout)">
|
||||||
<div className="flex min-h-[39.5px] flex-col">
|
<div className="flex min-h-[39.5px] flex-col">
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<fieldset
|
<fieldset
|
||||||
|
@ -691,20 +676,19 @@ export default function WebRTCVideo() {
|
||||||
<div
|
<div
|
||||||
className={cx(
|
className={cx(
|
||||||
"absolute inset-0 -z-0 bg-blue-50/40 opacity-80 dark:bg-slate-800/40",
|
"absolute inset-0 -z-0 bg-blue-50/40 opacity-80 dark:bg-slate-800/40",
|
||||||
"[background-image:radial-gradient(theme(colors.blue.300)_0.5px,transparent_0.5px),radial-gradient(theme(colors.blue.300)_0.5px,transparent_0.5px)] dark:[background-image:radial-gradient(theme(colors.slate.700)_0.5px,transparent_0.5px),radial-gradient(theme(colors.slate.700)_0.5px,transparent_0.5px)]",
|
"bg-[radial-gradient(var(--color-blue-300)_0.5px,transparent_0.5px),radial-gradient(var(--color-blue-300)_0.5px,transparent_0.5px)] dark:bg-[radial-gradient(var(--color-slate-700)_0.5px,transparent_0.5px),radial-gradient(var(--color-slate-700)_0.5px,transparent_0.5px)]",
|
||||||
"[background-position:0_0,10px_10px]",
|
"bg-position-[0_0,10px_10px]",
|
||||||
"[background-size:20px_20px]",
|
"bg-size-[20px_20px]",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<div className="flex h-full flex-col">
|
<div className="flex h-full flex-col">
|
||||||
<div className="relative flex-grow overflow-hidden">
|
<div className="relative grow overflow-hidden">
|
||||||
<div className="flex h-full flex-col">
|
<div className="flex h-full flex-col">
|
||||||
<div className="grid flex-grow grid-rows-bodyFooter overflow-hidden">
|
<div className="grid grow grid-rows-(--grid-bodyFooter) overflow-hidden">
|
||||||
<div className="relative mx-4 my-2 flex items-center justify-center overflow-hidden">
|
|
||||||
<div className="relative flex h-full w-full items-center justify-center">
|
|
||||||
<div className="relative inline-block">
|
|
||||||
{/* In relative mouse mode and under https, we enable the pointer lock, and to do so we need a bar to show the user to click on the video to enable mouse control */}
|
{/* In relative mouse mode and under https, we enable the pointer lock, and to do so we need a bar to show the user to click on the video to enable mouse control */}
|
||||||
<PointerLockBar show={showPointerLockBar} />
|
<PointerLockBar show={showPointerLockBar} />
|
||||||
|
<div className="relative mx-4 my-2 flex items-center justify-center overflow-hidden">
|
||||||
|
<div className="relative flex h-full w-full items-center justify-center">
|
||||||
<video
|
<video
|
||||||
ref={videoElm}
|
ref={videoElm}
|
||||||
autoPlay={true}
|
autoPlay={true}
|
||||||
|
@ -715,16 +699,19 @@ export default function WebRTCVideo() {
|
||||||
playsInline
|
playsInline
|
||||||
disablePictureInPicture
|
disablePictureInPicture
|
||||||
controlsList="nofullscreen"
|
controlsList="nofullscreen"
|
||||||
|
style={{
|
||||||
|
filter: `saturate(${videoSaturation}) brightness(${videoBrightness}) contrast(${videoContrast})`,
|
||||||
|
}}
|
||||||
className={cx(
|
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",
|
"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,
|
"cursor-none": settings.isCursorHidden,
|
||||||
"opacity-0":
|
"opacity-0":
|
||||||
isVideoLoading ||
|
isVideoLoading ||
|
||||||
hdmiError ||
|
hdmiError ||
|
||||||
peerConnectionState !== "connected",
|
peerConnectionState !== "connected",
|
||||||
"!opacity-60": showPointerLockBar,
|
"opacity-60!": showPointerLockBar,
|
||||||
"animate-slideUpFade border border-slate-800/30 opacity-0 shadow dark:border-slate-300/20":
|
"animate-slideUpFade border border-slate-800/30 shadow-xs dark:border-slate-300/20":
|
||||||
isPlaying,
|
isPlaying,
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
|
@ -732,7 +719,7 @@ export default function WebRTCVideo() {
|
||||||
{peerConnection?.connectionState == "connected" && (
|
{peerConnection?.connectionState == "connected" && (
|
||||||
<div
|
<div
|
||||||
style={{ animationDuration: "500ms" }}
|
style={{ animationDuration: "500ms" }}
|
||||||
className="pointer-events-none absolute inset-0 flex animate-slideUpFade items-center justify-center opacity-0"
|
className="animate-slideUpFade pointer-events-none absolute inset-0 flex items-center justify-center"
|
||||||
>
|
>
|
||||||
<div className="relative h-full w-full rounded-md">
|
<div className="relative h-full w-full rounded-md">
|
||||||
<LoadingVideoOverlay show={isVideoLoading} />
|
<LoadingVideoOverlay show={isVideoLoading} />
|
||||||
|
@ -748,7 +735,6 @@ export default function WebRTCVideo() {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<VirtualKeyboard />
|
<VirtualKeyboard />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -84,7 +84,7 @@ export default function ExtensionPopover() {
|
||||||
return (
|
return (
|
||||||
<GridCard>
|
<GridCard>
|
||||||
<div className="space-y-4 p-4 py-3">
|
<div className="space-y-4 p-4 py-3">
|
||||||
<div className="grid h-full grid-rows-headerBody">
|
<div className="grid h-full grid-rows-(--grid-headerBody)">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{activeExtension ? (
|
{activeExtension ? (
|
||||||
// Extension Control View
|
// Extension Control View
|
||||||
|
@ -92,7 +92,7 @@ export default function ExtensionPopover() {
|
||||||
{renderActiveExtension()}
|
{renderActiveExtension()}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="flex animate-fadeIn items-center justify-end space-x-2 opacity-0"
|
className="flex animate-fadeIn opacity-0 items-center justify-end space-x-2"
|
||||||
style={{
|
style={{
|
||||||
animationDuration: "0.7s",
|
animationDuration: "0.7s",
|
||||||
animationDelay: "0.2s",
|
animationDelay: "0.2s",
|
||||||
|
@ -113,7 +113,7 @@ export default function ExtensionPopover() {
|
||||||
title="Extensions"
|
title="Extensions"
|
||||||
description="Load and manage your extensions"
|
description="Load and manage your extensions"
|
||||||
/>
|
/>
|
||||||
<Card className="animate-fadeIn opacity-0">
|
<Card className="animate-fadeIn opacity-0" >
|
||||||
<div className="w-full divide-y divide-slate-700/30 dark:divide-slate-600/30">
|
<div className="w-full divide-y divide-slate-700/30 dark:divide-slate-600/30">
|
||||||
{AVAILABLE_EXTENSIONS.map(extension => (
|
{AVAILABLE_EXTENSIONS.map(extension => (
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -194,7 +194,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
||||||
return (
|
return (
|
||||||
<GridCard>
|
<GridCard>
|
||||||
<div className="space-y-4 p-4 py-3">
|
<div className="space-y-4 p-4 py-3">
|
||||||
<div ref={ref} className="grid h-full grid-rows-headerBody">
|
<div ref={ref} className="grid h-full grid-rows-(--grid-headerBody)">
|
||||||
<div className="h-full space-y-4">
|
<div className="h-full space-y-4">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<SettingsPageHeader
|
<SettingsPageHeader
|
||||||
|
@ -214,7 +214,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="animate-fadeIn space-y-2 opacity-0"
|
className="animate-fadeIn opacity-0 space-y-2"
|
||||||
style={{
|
style={{
|
||||||
animationDuration: "0.7s",
|
animationDuration: "0.7s",
|
||||||
animationDelay: "0.1s",
|
animationDelay: "0.1s",
|
||||||
|
@ -289,7 +289,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
||||||
|
|
||||||
{!remoteVirtualMediaState && (
|
{!remoteVirtualMediaState && (
|
||||||
<div
|
<div
|
||||||
className="flex animate-fadeIn items-center justify-end space-x-2 opacity-0"
|
className="flex animate-fadeIn opacity-0 items-center justify-end space-x-2"
|
||||||
style={{
|
style={{
|
||||||
animationDuration: "0.7s",
|
animationDuration: "0.7s",
|
||||||
animationDelay: "0.2s",
|
animationDelay: "0.2s",
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { LuCornerDownLeft } from "react-icons/lu";
|
import { LuCornerDownLeft } from "react-icons/lu";
|
||||||
import { ExclamationCircleIcon } from "@heroicons/react/16/solid";
|
import { ExclamationCircleIcon } from "@heroicons/react/16/solid";
|
||||||
import { useClose } from "@headlessui/react";
|
import { useClose } from "@headlessui/react";
|
||||||
|
@ -8,14 +8,21 @@ import { GridCard } from "@components/Card";
|
||||||
import { TextAreaWithLabel } from "@components/TextArea";
|
import { TextAreaWithLabel } from "@components/TextArea";
|
||||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||||
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||||
import { useHidStore, useRTCStore, useUiStore } from "@/hooks/stores";
|
import { useHidStore, useRTCStore, useUiStore, useSettingsStore } from "@/hooks/stores";
|
||||||
import { chars, keys, modifiers } from "@/keyboardMappings";
|
import { keys, modifiers } from "@/keyboardMappings";
|
||||||
|
import { layouts, chars } from "@/keyboardLayouts";
|
||||||
import notifications from "@/notifications";
|
import notifications from "@/notifications";
|
||||||
|
|
||||||
const hidKeyboardPayload = (keys: number[], modifier: number) => {
|
const hidKeyboardPayload = (keys: number[], modifier: number) => {
|
||||||
return { keys, modifier };
|
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() {
|
export default function PasteModal() {
|
||||||
const TextAreaRef = useRef<HTMLTextAreaElement>(null);
|
const TextAreaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
const setPasteMode = useHidStore(state => state.setPasteModeEnabled);
|
const setPasteMode = useHidStore(state => state.setPasteModeEnabled);
|
||||||
|
@ -27,6 +34,25 @@ export default function PasteModal() {
|
||||||
const [invalidChars, setInvalidChars] = useState<string[]>([]);
|
const [invalidChars, setInvalidChars] = useState<string[]>([]);
|
||||||
const close = useClose();
|
const close = useClose();
|
||||||
|
|
||||||
|
const keyboardLayout = useSettingsStore(state => state.keyboardLayout);
|
||||||
|
const setKeyboardLayout = useSettingsStore(
|
||||||
|
state => state.setKeyboardLayout,
|
||||||
|
);
|
||||||
|
|
||||||
|
// this ensures we always get the original en_US if it hasn't been set yet
|
||||||
|
const safeKeyboardLayout = useMemo(() => {
|
||||||
|
if (keyboardLayout && keyboardLayout.length > 0)
|
||||||
|
return keyboardLayout;
|
||||||
|
return "en_US";
|
||||||
|
}, [keyboardLayout]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
send("getKeyboardLayout", {}, resp => {
|
||||||
|
if ("error" in resp) return;
|
||||||
|
setKeyboardLayout(resp.result as string);
|
||||||
|
});
|
||||||
|
}, [send, setKeyboardLayout]);
|
||||||
|
|
||||||
const onCancelPasteMode = useCallback(() => {
|
const onCancelPasteMode = useCallback(() => {
|
||||||
setPasteMode(false);
|
setPasteMode(false);
|
||||||
setDisableVideoFocusTrap(false);
|
setDisableVideoFocusTrap(false);
|
||||||
|
@ -37,18 +63,32 @@ export default function PasteModal() {
|
||||||
setPasteMode(false);
|
setPasteMode(false);
|
||||||
setDisableVideoFocusTrap(false);
|
setDisableVideoFocusTrap(false);
|
||||||
if (rpcDataChannel?.readyState !== "open" || !TextAreaRef.current) return;
|
if (rpcDataChannel?.readyState !== "open" || !TextAreaRef.current) return;
|
||||||
|
if (!safeKeyboardLayout) return;
|
||||||
|
if (!chars[safeKeyboardLayout]) return;
|
||||||
const text = TextAreaRef.current.value;
|
const text = TextAreaRef.current.value;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for (const char of text) {
|
for (const char of text) {
|
||||||
const { key, shift } = chars[char] ?? {};
|
const { key, shift, altRight, deadKey, accentKey } = chars[safeKeyboardLayout][char]
|
||||||
if (!key) continue;
|
if (!key) continue;
|
||||||
|
|
||||||
|
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) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
send(
|
send(
|
||||||
"keyboardReport",
|
"keyboardReport",
|
||||||
hidKeyboardPayload([keys[key]], shift ? modifiers["ShiftLeft"] : 0),
|
hidKeyboardPayload([kei], modz[index]),
|
||||||
params => {
|
params => {
|
||||||
if ("error" in params) return reject(params.error);
|
if ("error" in params) return reject(params.error);
|
||||||
send("keyboardReport", hidKeyboardPayload([], 0), params => {
|
send("keyboardReport", hidKeyboardPayload([], 0), params => {
|
||||||
|
@ -59,11 +99,12 @@ export default function PasteModal() {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
notifications.error("Failed to paste text");
|
notifications.error("Failed to paste text");
|
||||||
}
|
}
|
||||||
}, [rpcDataChannel?.readyState, send, setDisableVideoFocusTrap, setPasteMode]);
|
}, [rpcDataChannel?.readyState, send, setDisableVideoFocusTrap, setPasteMode, safeKeyboardLayout]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (TextAreaRef.current) {
|
if (TextAreaRef.current) {
|
||||||
|
@ -74,7 +115,7 @@ export default function PasteModal() {
|
||||||
return (
|
return (
|
||||||
<GridCard>
|
<GridCard>
|
||||||
<div className="space-y-4 p-4 py-3">
|
<div className="space-y-4 p-4 py-3">
|
||||||
<div className="grid h-full grid-rows-headerBody">
|
<div className="grid h-full grid-rows-(--grid-headerBody)">
|
||||||
<div className="h-full space-y-4">
|
<div className="h-full space-y-4">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<SettingsPageHeader
|
<SettingsPageHeader
|
||||||
|
@ -83,14 +124,14 @@ export default function PasteModal() {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="animate-fadeIn space-y-2 opacity-0"
|
className="animate-fadeIn opacity-0 space-y-2"
|
||||||
style={{
|
style={{
|
||||||
animationDuration: "0.7s",
|
animationDuration: "0.7s",
|
||||||
animationDelay: "0.1s",
|
animationDelay: "0.1s",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<div className="w-full" onKeyUp={e => e.stopPropagation()}>
|
<div className="w-full" onKeyUp={e => e.stopPropagation()} onKeyDown={e => e.stopPropagation()}>
|
||||||
<TextAreaWithLabel
|
<TextAreaWithLabel
|
||||||
ref={TextAreaRef}
|
ref={TextAreaRef}
|
||||||
label="Paste from host"
|
label="Paste from host"
|
||||||
|
@ -113,7 +154,7 @@ export default function PasteModal() {
|
||||||
// @ts-expect-error TS doesn't recognize Intl.Segmenter in some environments
|
// @ts-expect-error TS doesn't recognize Intl.Segmenter in some environments
|
||||||
[...new Intl.Segmenter().segment(value)]
|
[...new Intl.Segmenter().segment(value)]
|
||||||
.map(x => x.segment)
|
.map(x => x.segment)
|
||||||
.filter(char => !chars[char]),
|
.filter(char => !chars[safeKeyboardLayout][char]),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -132,12 +173,17 @@ export default function PasteModal() {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<p className="text-xs text-slate-600 dark:text-slate-400">
|
||||||
|
Sending text using keyboard layout: {layouts[safeKeyboardLayout]}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="flex animate-fadeIn items-center justify-end gap-x-2 opacity-0"
|
className="flex animate-fadeIn opacity-0 items-center justify-end gap-x-2"
|
||||||
style={{
|
style={{
|
||||||
animationDuration: "0.7s",
|
animationDuration: "0.7s",
|
||||||
animationDelay: "0.2s",
|
animationDelay: "0.2s",
|
||||||
|
|
|
@ -26,7 +26,7 @@ export default function AddDeviceForm({
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div
|
<div
|
||||||
className="animate-fadeIn space-y-4 opacity-0"
|
className="animate-fadeIn opacity-0 space-y-4"
|
||||||
style={{
|
style={{
|
||||||
animationDuration: "0.5s",
|
animationDuration: "0.5s",
|
||||||
animationFillMode: "forwards",
|
animationFillMode: "forwards",
|
||||||
|
@ -73,7 +73,7 @@ export default function AddDeviceForm({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="flex animate-fadeIn items-center justify-end space-x-2 opacity-0"
|
className="flex animate-fadeIn opacity-0 items-center justify-end space-x-2"
|
||||||
style={{
|
style={{
|
||||||
animationDuration: "0.7s",
|
animationDuration: "0.7s",
|
||||||
animationDelay: "0.2s",
|
animationDelay: "0.2s",
|
||||||
|
|
|
@ -63,7 +63,7 @@ export default function DeviceList({
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
<div
|
<div
|
||||||
className="flex animate-fadeIn items-center justify-end space-x-2 opacity-0"
|
className="flex animate-fadeIn opacity-0 items-center justify-end space-x-2"
|
||||||
style={{
|
style={{
|
||||||
animationDuration: "0.7s",
|
animationDuration: "0.7s",
|
||||||
animationDelay: "0.2s",
|
animationDelay: "0.2s",
|
||||||
|
|
|
@ -35,7 +35,7 @@ export default function EmptyStateCard({
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
<div
|
<div
|
||||||
className="flex animate-fadeIn items-center justify-end space-x-2 opacity-0"
|
className="flex animate-fadeIn opacity-0 items-center justify-end space-x-2"
|
||||||
style={{
|
style={{
|
||||||
animationDuration: "0.7s",
|
animationDuration: "0.7s",
|
||||||
animationDelay: "0.2s",
|
animationDelay: "0.2s",
|
||||||
|
|
|
@ -102,7 +102,7 @@ export default function WakeOnLanModal() {
|
||||||
return (
|
return (
|
||||||
<GridCard>
|
<GridCard>
|
||||||
<div className="space-y-4 p-4 py-3">
|
<div className="space-y-4 p-4 py-3">
|
||||||
<div className="grid h-full grid-rows-headerBody">
|
<div className="grid h-full grid-rows-(--grid-headerBody)">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<SettingsPageHeader
|
<SettingsPageHeader
|
||||||
title="Wake On LAN"
|
title="Wake On LAN"
|
||||||
|
|
|
@ -99,7 +99,7 @@ export default function ConnectionStatsSidebar() {
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid h-full grid-rows-headerBody shadow-sm">
|
<div className="grid h-full grid-rows-(--grid-headerBody) shadow-xs">
|
||||||
<SidebarHeader title="Connection Stats" setSidebarView={setSidebarView} />
|
<SidebarHeader title="Connection Stats" setSidebarView={setSidebarView} />
|
||||||
<div className="h-full space-y-4 overflow-y-scroll bg-white px-4 py-2 pb-8 dark:bg-slate-900">
|
<div className="h-full space-y-4 overflow-y-scroll bg-white px-4 py-2 pb-8 dark:bg-slate-900">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
import { createJSONStorage, persist } from "zustand/middleware";
|
import { createJSONStorage, persist } from "zustand/middleware";
|
||||||
import { MAX_STEPS_PER_MACRO, MAX_TOTAL_MACROS, MAX_KEYS_PER_STEP } from "@/constants/macros";
|
|
||||||
|
import {
|
||||||
|
MAX_STEPS_PER_MACRO,
|
||||||
|
MAX_TOTAL_MACROS,
|
||||||
|
MAX_KEYS_PER_STEP,
|
||||||
|
} from "@/constants/macros";
|
||||||
|
|
||||||
// Define the JsonRpc types for better type checking
|
// Define the JsonRpc types for better type checking
|
||||||
interface JsonRpcResponse {
|
interface JsonRpcResponse {
|
||||||
|
@ -278,6 +283,8 @@ export const useVideoStore = create<VideoState>(set => ({
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
export type KeyboardLedSync = "auto" | "browser" | "host";
|
||||||
|
|
||||||
interface SettingsState {
|
interface SettingsState {
|
||||||
isCursorHidden: boolean;
|
isCursorHidden: boolean;
|
||||||
setCursorVisibility: (enabled: boolean) => void;
|
setCursorVisibility: (enabled: boolean) => void;
|
||||||
|
@ -297,6 +304,29 @@ interface SettingsState {
|
||||||
|
|
||||||
backlightSettings: BacklightSettings;
|
backlightSettings: BacklightSettings;
|
||||||
setBacklightSettings: (settings: BacklightSettings) => void;
|
setBacklightSettings: (settings: BacklightSettings) => void;
|
||||||
|
|
||||||
|
keyboardLayout: string;
|
||||||
|
setKeyboardLayout: (layout: string) => void;
|
||||||
|
|
||||||
|
actionBarCtrlAltDel: boolean;
|
||||||
|
setActionBarCtrlAltDel: (enabled: boolean) => void;
|
||||||
|
|
||||||
|
keyboardLedSync: KeyboardLedSync;
|
||||||
|
setKeyboardLedSync: (sync: KeyboardLedSync) => void;
|
||||||
|
|
||||||
|
scrollThrottling: number;
|
||||||
|
setScrollThrottling: (value: number) => void;
|
||||||
|
|
||||||
|
showPressedKeys: boolean;
|
||||||
|
setShowPressedKeys: (show: boolean) => void;
|
||||||
|
|
||||||
|
// Video enhancement settings
|
||||||
|
videoSaturation: number;
|
||||||
|
setVideoSaturation: (value: number) => void;
|
||||||
|
videoBrightness: number;
|
||||||
|
setVideoBrightness: (value: number) => void;
|
||||||
|
videoContrast: number;
|
||||||
|
setVideoContrast: (value: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useSettingsStore = create(
|
export const useSettingsStore = create(
|
||||||
|
@ -316,8 +346,7 @@ export const useSettingsStore = create(
|
||||||
setDeveloperMode: enabled => set({ developerMode: enabled }),
|
setDeveloperMode: enabled => set({ developerMode: enabled }),
|
||||||
|
|
||||||
displayRotation: "270",
|
displayRotation: "270",
|
||||||
setDisplayRotation: (rotation: string) =>
|
setDisplayRotation: (rotation: string) => set({ displayRotation: rotation }),
|
||||||
set({ displayRotation: rotation }),
|
|
||||||
|
|
||||||
backlightSettings: {
|
backlightSettings: {
|
||||||
max_brightness: 100,
|
max_brightness: 100,
|
||||||
|
@ -326,6 +355,29 @@ export const useSettingsStore = create(
|
||||||
},
|
},
|
||||||
setBacklightSettings: (settings: BacklightSettings) =>
|
setBacklightSettings: (settings: BacklightSettings) =>
|
||||||
set({ backlightSettings: settings }),
|
set({ backlightSettings: settings }),
|
||||||
|
|
||||||
|
keyboardLayout: "en-US",
|
||||||
|
setKeyboardLayout: layout => set({ keyboardLayout: layout }),
|
||||||
|
|
||||||
|
actionBarCtrlAltDel: false,
|
||||||
|
setActionBarCtrlAltDel: enabled => set({ actionBarCtrlAltDel: enabled }),
|
||||||
|
|
||||||
|
keyboardLedSync: "auto",
|
||||||
|
setKeyboardLedSync: sync => set({ keyboardLedSync: sync }),
|
||||||
|
|
||||||
|
scrollThrottling: 0,
|
||||||
|
setScrollThrottling: value => set({ scrollThrottling: value }),
|
||||||
|
|
||||||
|
showPressedKeys: true,
|
||||||
|
setShowPressedKeys: show => set({ showPressedKeys: show }),
|
||||||
|
|
||||||
|
// Video enhancement settings with default values (1.0 = normal)
|
||||||
|
videoSaturation: 1.0,
|
||||||
|
setVideoSaturation: value => set({ videoSaturation: value }),
|
||||||
|
videoBrightness: 1.0,
|
||||||
|
setVideoBrightness: value => set({ videoBrightness: value }),
|
||||||
|
videoContrast: 1.0,
|
||||||
|
setVideoContrast: value => set({ videoContrast: value }),
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: "settings",
|
name: "settings",
|
||||||
|
@ -334,78 +386,6 @@ export const useSettingsStore = create(
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
export interface DeviceSettingsState {
|
|
||||||
trackpadSensitivity: number;
|
|
||||||
mouseSensitivity: number;
|
|
||||||
clampMin: number;
|
|
||||||
clampMax: number;
|
|
||||||
blockDelay: number;
|
|
||||||
trackpadThreshold: number;
|
|
||||||
scrollSensitivity: "low" | "default" | "high";
|
|
||||||
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 {
|
export interface RemoteVirtualMediaState {
|
||||||
source: "WebRTC" | "HTTP" | "Storage" | null;
|
source: "WebRTC" | "HTTP" | "Storage" | null;
|
||||||
mode: "CDROM" | "Disk" | null;
|
mode: "CDROM" | "Disk" | null;
|
||||||
|
@ -456,6 +436,21 @@ export const useMountMediaStore = create<MountMediaState>(set => ({
|
||||||
setErrorMessage: message => set({ errorMessage: message }),
|
setErrorMessage: message => set({ errorMessage: message }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
export interface KeyboardLedState {
|
||||||
|
num_lock: boolean;
|
||||||
|
caps_lock: boolean;
|
||||||
|
scroll_lock: boolean;
|
||||||
|
compose: boolean;
|
||||||
|
kana: boolean;
|
||||||
|
};
|
||||||
|
const defaultKeyboardLedState: KeyboardLedState = {
|
||||||
|
num_lock: false,
|
||||||
|
caps_lock: false,
|
||||||
|
scroll_lock: false,
|
||||||
|
compose: false,
|
||||||
|
kana: false,
|
||||||
|
};
|
||||||
|
|
||||||
export interface HidState {
|
export interface HidState {
|
||||||
activeKeys: number[];
|
activeKeys: number[];
|
||||||
activeModifiers: number[];
|
activeModifiers: number[];
|
||||||
|
@ -474,18 +469,18 @@ export interface HidState {
|
||||||
altGrCtrlTime: number; // _altGrCtrlTime
|
altGrCtrlTime: number; // _altGrCtrlTime
|
||||||
setAltGrCtrlTime: (time: number) => void;
|
setAltGrCtrlTime: (time: number) => void;
|
||||||
|
|
||||||
isNumLockActive: boolean;
|
keyboardLedState?: KeyboardLedState;
|
||||||
setIsNumLockActive: (enabled: boolean) => void;
|
setKeyboardLedState: (state: KeyboardLedState) => void;
|
||||||
|
setIsNumLockActive: (active: boolean) => void;
|
||||||
|
setIsCapsLockActive: (active: boolean) => void;
|
||||||
|
setIsScrollLockActive: (active: boolean) => void;
|
||||||
|
|
||||||
isScrollLockActive: boolean;
|
keyboardLedStateSyncAvailable: boolean;
|
||||||
setIsScrollLockActive: (enabled: boolean) => void;
|
setKeyboardLedStateSyncAvailable: (available: boolean) => void;
|
||||||
|
|
||||||
isVirtualKeyboardEnabled: boolean;
|
isVirtualKeyboardEnabled: boolean;
|
||||||
setVirtualKeyboardEnabled: (enabled: boolean) => void;
|
setVirtualKeyboardEnabled: (enabled: boolean) => void;
|
||||||
|
|
||||||
isCapsLockActive: boolean;
|
|
||||||
setIsCapsLockActive: (enabled: boolean) => void;
|
|
||||||
|
|
||||||
isPasteModeEnabled: boolean;
|
isPasteModeEnabled: boolean;
|
||||||
setPasteModeEnabled: (enabled: boolean) => void;
|
setPasteModeEnabled: (enabled: boolean) => void;
|
||||||
|
|
||||||
|
@ -493,7 +488,7 @@ export interface HidState {
|
||||||
setUsbState: (state: HidState["usbState"]) => void;
|
setUsbState: (state: HidState["usbState"]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useHidStore = create<HidState>(set => ({
|
export const useHidStore = create<HidState>((set, get) => ({
|
||||||
activeKeys: [],
|
activeKeys: [],
|
||||||
activeModifiers: [],
|
activeModifiers: [],
|
||||||
updateActiveKeysAndModifiers: ({ keys, modifiers }) => {
|
updateActiveKeysAndModifiers: ({ keys, modifiers }) => {
|
||||||
|
@ -509,18 +504,29 @@ export const useHidStore = create<HidState>(set => ({
|
||||||
altGrCtrlTime: 0,
|
altGrCtrlTime: 0,
|
||||||
setAltGrCtrlTime: time => set({ altGrCtrlTime: time }),
|
setAltGrCtrlTime: time => set({ altGrCtrlTime: time }),
|
||||||
|
|
||||||
isNumLockActive: false,
|
setKeyboardLedState: ledState => set({ keyboardLedState: ledState }),
|
||||||
setIsNumLockActive: enabled => set({ isNumLockActive: enabled }),
|
setIsNumLockActive: active => {
|
||||||
|
const keyboardLedState = { ...(get().keyboardLedState || defaultKeyboardLedState) };
|
||||||
|
keyboardLedState.num_lock = active;
|
||||||
|
set({ keyboardLedState });
|
||||||
|
},
|
||||||
|
setIsCapsLockActive: active => {
|
||||||
|
const keyboardLedState = { ...(get().keyboardLedState || defaultKeyboardLedState) };
|
||||||
|
keyboardLedState.caps_lock = active;
|
||||||
|
set({ keyboardLedState });
|
||||||
|
},
|
||||||
|
setIsScrollLockActive: active => {
|
||||||
|
const keyboardLedState = { ...(get().keyboardLedState || defaultKeyboardLedState) };
|
||||||
|
keyboardLedState.scroll_lock = active;
|
||||||
|
set({ keyboardLedState });
|
||||||
|
},
|
||||||
|
|
||||||
isScrollLockActive: false,
|
keyboardLedStateSyncAvailable: false,
|
||||||
setIsScrollLockActive: enabled => set({ isScrollLockActive: enabled }),
|
setKeyboardLedStateSyncAvailable: available => set({ keyboardLedStateSyncAvailable: available }),
|
||||||
|
|
||||||
isVirtualKeyboardEnabled: false,
|
isVirtualKeyboardEnabled: false,
|
||||||
setVirtualKeyboardEnabled: enabled => set({ isVirtualKeyboardEnabled: enabled }),
|
setVirtualKeyboardEnabled: enabled => set({ isVirtualKeyboardEnabled: enabled }),
|
||||||
|
|
||||||
isCapsLockActive: false,
|
|
||||||
setIsCapsLockActive: enabled => set({ isCapsLockActive: enabled }),
|
|
||||||
|
|
||||||
isPasteModeEnabled: false,
|
isPasteModeEnabled: false,
|
||||||
setPasteModeEnabled: enabled => set({ isPasteModeEnabled: enabled }),
|
setPasteModeEnabled: enabled => set({ isPasteModeEnabled: enabled }),
|
||||||
|
|
||||||
|
@ -726,12 +732,23 @@ export interface NetworkState {
|
||||||
setDhcpLeaseExpiry: (expiry: Date) => void;
|
setDhcpLeaseExpiry: (expiry: Date) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type IPv6Mode =
|
||||||
export type IPv6Mode = "disabled" | "slaac" | "dhcpv6" | "slaac_and_dhcpv6" | "static" | "link_local" | "unknown";
|
| "disabled"
|
||||||
|
| "slaac"
|
||||||
|
| "dhcpv6"
|
||||||
|
| "slaac_and_dhcpv6"
|
||||||
|
| "static"
|
||||||
|
| "link_local"
|
||||||
|
| "unknown";
|
||||||
export type IPv4Mode = "disabled" | "static" | "dhcp" | "unknown";
|
export type IPv4Mode = "disabled" | "static" | "dhcp" | "unknown";
|
||||||
export type LLDPMode = "disabled" | "basic" | "all" | "unknown";
|
export type LLDPMode = "disabled" | "basic" | "all" | "unknown";
|
||||||
export type mDNSMode = "disabled" | "auto" | "ipv4_only" | "ipv6_only" | "unknown";
|
export type mDNSMode = "disabled" | "auto" | "ipv4_only" | "ipv6_only" | "unknown";
|
||||||
export type TimeSyncMode = "ntp_only" | "ntp_and_http" | "http_only" | "custom" | "unknown";
|
export type TimeSyncMode =
|
||||||
|
| "ntp_only"
|
||||||
|
| "ntp_and_http"
|
||||||
|
| "http_only"
|
||||||
|
| "custom"
|
||||||
|
| "unknown";
|
||||||
|
|
||||||
export interface NetworkSettings {
|
export interface NetworkSettings {
|
||||||
hostname: string;
|
hostname: string;
|
||||||
|
@ -756,7 +773,7 @@ export const useNetworkStateStore = create<NetworkState>((set, get) => ({
|
||||||
|
|
||||||
lease.lease_expiry = expiry;
|
lease.lease_expiry = expiry;
|
||||||
set({ dhcp_lease: lease });
|
set({ dhcp_lease: lease });
|
||||||
}
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export interface KeySequenceStep {
|
export interface KeySequenceStep {
|
||||||
|
@ -778,8 +795,20 @@ export interface MacrosState {
|
||||||
initialized: boolean;
|
initialized: boolean;
|
||||||
loadMacros: () => Promise<void>;
|
loadMacros: () => Promise<void>;
|
||||||
saveMacros: (macros: KeySequence[]) => Promise<void>;
|
saveMacros: (macros: KeySequence[]) => Promise<void>;
|
||||||
sendFn: ((method: string, params: unknown, callback?: ((resp: JsonRpcResponse) => void) | undefined) => void) | null;
|
sendFn:
|
||||||
setSendFn: (sendFn: ((method: string, params: unknown, callback?: ((resp: JsonRpcResponse) => void) | undefined) => void)) => void;
|
| ((
|
||||||
|
method: string,
|
||||||
|
params: unknown,
|
||||||
|
callback?: ((resp: JsonRpcResponse) => void) | undefined,
|
||||||
|
) => void)
|
||||||
|
| null;
|
||||||
|
setSendFn: (
|
||||||
|
sendFn: (
|
||||||
|
method: string,
|
||||||
|
params: unknown,
|
||||||
|
callback?: ((resp: JsonRpcResponse) => void) | undefined,
|
||||||
|
) => void,
|
||||||
|
) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const generateMacroId = () => {
|
export const generateMacroId = () => {
|
||||||
|
@ -792,7 +821,7 @@ export const useMacrosStore = create<MacrosState>((set, get) => ({
|
||||||
initialized: false,
|
initialized: false,
|
||||||
sendFn: null,
|
sendFn: null,
|
||||||
|
|
||||||
setSendFn: (sendFn) => {
|
setSendFn: sendFn => {
|
||||||
set({ sendFn });
|
set({ sendFn });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -809,7 +838,7 @@ export const useMacrosStore = create<MacrosState>((set, get) => ({
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
sendFn("getKeyboardMacros", {}, (response) => {
|
sendFn("getKeyboardMacros", {}, response => {
|
||||||
if (response.error) {
|
if (response.error) {
|
||||||
console.error("Error loading macros:", response.error);
|
console.error("Error loading macros:", response.error);
|
||||||
reject(new Error(response.error.message));
|
reject(new Error(response.error.message));
|
||||||
|
@ -829,7 +858,7 @@ export const useMacrosStore = create<MacrosState>((set, get) => ({
|
||||||
|
|
||||||
set({
|
set({
|
||||||
macros: sortedMacros,
|
macros: sortedMacros,
|
||||||
initialized: true
|
initialized: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
resolve();
|
resolve();
|
||||||
|
@ -856,15 +885,23 @@ export const useMacrosStore = create<MacrosState>((set, get) => ({
|
||||||
|
|
||||||
for (const macro of macros) {
|
for (const macro of macros) {
|
||||||
if (macro.steps.length > MAX_STEPS_PER_MACRO) {
|
if (macro.steps.length > MAX_STEPS_PER_MACRO) {
|
||||||
console.error(`Cannot save: macro "${macro.name}" exceeds maximum of ${MAX_STEPS_PER_MACRO} steps`);
|
console.error(
|
||||||
throw new Error(`Cannot save: macro "${macro.name}" exceeds maximum of ${MAX_STEPS_PER_MACRO} steps`);
|
`Cannot save: macro "${macro.name}" exceeds maximum of ${MAX_STEPS_PER_MACRO} steps`,
|
||||||
|
);
|
||||||
|
throw new Error(
|
||||||
|
`Cannot save: macro "${macro.name}" exceeds maximum of ${MAX_STEPS_PER_MACRO} steps`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < macro.steps.length; i++) {
|
for (let i = 0; i < macro.steps.length; i++) {
|
||||||
const step = macro.steps[i];
|
const step = macro.steps[i];
|
||||||
if (step.keys && step.keys.length > MAX_KEYS_PER_STEP) {
|
if (step.keys && step.keys.length > MAX_KEYS_PER_STEP) {
|
||||||
console.error(`Cannot save: macro "${macro.name}" step ${i + 1} exceeds maximum of ${MAX_KEYS_PER_STEP} keys`);
|
console.error(
|
||||||
throw new Error(`Cannot save: macro "${macro.name}" step ${i + 1} exceeds maximum of ${MAX_KEYS_PER_STEP} keys`);
|
`Cannot save: macro "${macro.name}" step ${i + 1} exceeds maximum of ${MAX_KEYS_PER_STEP} keys`,
|
||||||
|
);
|
||||||
|
throw new Error(
|
||||||
|
`Cannot save: macro "${macro.name}" step ${i + 1} exceeds maximum of ${MAX_KEYS_PER_STEP} keys`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -874,18 +911,23 @@ export const useMacrosStore = create<MacrosState>((set, get) => ({
|
||||||
try {
|
try {
|
||||||
const macrosWithSortOrder = macros.map((macro, index) => ({
|
const macrosWithSortOrder = macros.map((macro, index) => ({
|
||||||
...macro,
|
...macro,
|
||||||
sortOrder: macro.sortOrder !== undefined ? macro.sortOrder : index
|
sortOrder: macro.sortOrder !== undefined ? macro.sortOrder : index,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const response = await new Promise<JsonRpcResponse>((resolve) => {
|
const response = await new Promise<JsonRpcResponse>(resolve => {
|
||||||
sendFn("setKeyboardMacros", { params: { macros: macrosWithSortOrder } }, (response) => {
|
sendFn(
|
||||||
|
"setKeyboardMacros",
|
||||||
|
{ params: { macros: macrosWithSortOrder } },
|
||||||
|
response => {
|
||||||
resolve(response);
|
resolve(response);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.error) {
|
if (response.error) {
|
||||||
console.error("Error saving macros:", response.error);
|
console.error("Error saving macros:", response.error);
|
||||||
const errorMessage = typeof response.error.data === 'string'
|
const errorMessage =
|
||||||
|
typeof response.error.data === "string"
|
||||||
? response.error.data
|
? response.error.data
|
||||||
: response.error.message || "Failed to save macros";
|
: response.error.message || "Failed to save macros";
|
||||||
throw new Error(errorMessage);
|
throw new Error(errorMessage);
|
||||||
|
@ -899,5 +941,5 @@ export const useMacrosStore = create<MacrosState>((set, get) => ({
|
||||||
} finally {
|
} finally {
|
||||||
set({ loading: false });
|
set({ loading: false });
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}));
|
}));
|
170
ui/src/index.css
170
ui/src/index.css
|
@ -1,6 +1,144 @@
|
||||||
@tailwind base;
|
@import "tailwindcss";
|
||||||
@tailwind components;
|
|
||||||
@tailwind utilities;
|
@config "../tailwind.config.js";
|
||||||
|
|
||||||
|
@plugin "@tailwindcss/typography";
|
||||||
|
@plugin "@tailwindcss/forms";
|
||||||
|
@plugin "@headlessui/tailwindcss";
|
||||||
|
|
||||||
|
/* Dark mode uses CSS selector instead of prefers-color-scheme */
|
||||||
|
@custom-variant dark (&:where(.dark, .dark *));
|
||||||
|
|
||||||
|
@theme {
|
||||||
|
--font-sans: "Circular", sans-serif;
|
||||||
|
--font-display: "Circular", sans-serif;
|
||||||
|
--font-serif: "Circular", serif;
|
||||||
|
--font-mono: "Source Code Pro Variable", monospace;
|
||||||
|
|
||||||
|
--grid-layout: auto 1fr auto;
|
||||||
|
--grid-headerBody: auto 1fr;
|
||||||
|
--grid-bodyFooter: 1fr auto;
|
||||||
|
--grid-sidebar: 1fr minmax(360px, 25%);
|
||||||
|
|
||||||
|
--breakpoint-xs: 480px;
|
||||||
|
--breakpoint-2xl: 1440px;
|
||||||
|
--breakpoint-3xl: 1920px;
|
||||||
|
--breakpoint-4xl: 2560px;
|
||||||
|
|
||||||
|
/* Migrated animations */
|
||||||
|
--animate-enter: enter 0.2s ease-out;
|
||||||
|
--animate-leave: leave 0.15s ease-in forwards;
|
||||||
|
--animate-fadeInScale: fadeInScale 1s ease-out forwards;
|
||||||
|
--animate-fadeInScaleFloat:
|
||||||
|
fadeInScaleFloat 1s ease-out forwards, float 3s ease-in-out infinite;
|
||||||
|
--animate-fadeIn: fadeIn 1s ease-out forwards;
|
||||||
|
--animate-slideUpFade: slideUpFade 1s ease-out forwards;
|
||||||
|
|
||||||
|
--container-8xl: 88rem;
|
||||||
|
--container-9xl: 96rem;
|
||||||
|
--container-10xl: 104rem;
|
||||||
|
--container-11xl: 112rem;
|
||||||
|
--container-12xl: 120rem;
|
||||||
|
|
||||||
|
/* Migrated keyframes */
|
||||||
|
@keyframes enter {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.9);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes leave {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.9);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeInScale {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeInScaleFloat {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.98) translateY(10px);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1) translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes float {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translateY(-10px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
}
|
||||||
|
70% {
|
||||||
|
opacity: 0.8;
|
||||||
|
transform: translateY(1px);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideUpFade {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* If we don't ignore this, Prettier will add a space between the value and the `ms`. Rendering the utility invalid. */
|
||||||
|
/* prettier-ignore */
|
||||||
|
@utility max-width-* {
|
||||||
|
max-width: --modifier(--container-*, [length], [*]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* If we don't ignore this, Prettier will add a space between the value and the `ms`. Rendering the utility invalid. */
|
||||||
|
/* prettier-ignore */
|
||||||
|
@utility animation-delay-* {
|
||||||
|
animation-delay: --value(integer)ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* If we don't ignore this, Prettier will add a space between the value and the `ms`. Rendering the utility invalid. */
|
||||||
|
/* prettier-ignore */
|
||||||
|
@utility animation-duration-* {
|
||||||
|
animation-duration: --value(integer)ms;
|
||||||
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
@apply scroll-smooth;
|
@apply scroll-smooth;
|
||||||
|
@ -15,13 +153,13 @@ body {
|
||||||
|
|
||||||
@property --grid-color-start {
|
@property --grid-color-start {
|
||||||
syntax: "<color>";
|
syntax: "<color>";
|
||||||
initial-value: theme("colors.blue.50/10");
|
initial-value: var(--color-blue-50/10);
|
||||||
inherits: false;
|
inherits: false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@property --grid-color-end {
|
@property --grid-color-end {
|
||||||
syntax: "<color>";
|
syntax: "<color>";
|
||||||
initial-value: theme("colors.blue.50/100");
|
initial-value: var(--color-blue-50/100);
|
||||||
inherits: false;
|
inherits: false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,8 +175,8 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
.group:hover .grid-card {
|
.group:hover .grid-card {
|
||||||
--grid-color-start: theme("colors.blue.100/50");
|
--grid-color-start: var(--color-blue-100/50);
|
||||||
--grid-color-end: theme("colors.blue.50/50");
|
--grid-color-end: var(--color-blue-50/50);
|
||||||
}
|
}
|
||||||
|
|
||||||
video::-webkit-media-controls {
|
video::-webkit-media-controls {
|
||||||
|
@ -46,11 +184,11 @@ video::-webkit-media-controls {
|
||||||
}
|
}
|
||||||
|
|
||||||
.hg-theme-default {
|
.hg-theme-default {
|
||||||
@apply !font-display font-normal;
|
@apply font-display! font-normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hg-theme-default .hg-button {
|
.hg-theme-default .hg-button {
|
||||||
@apply border !border-b border-slate-800/25 !border-b-slate-800/25 !shadow-sm;
|
@apply border border-b! border-slate-800/25 border-b-slate-800/25! shadow-xs!;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hg-theme-default .hg-button span {
|
.hg-theme-default .hg-button span {
|
||||||
|
@ -122,7 +260,7 @@ video::-webkit-media-controls {
|
||||||
}
|
}
|
||||||
|
|
||||||
.hg-button {
|
.hg-button {
|
||||||
@apply dark:!bg-slate-800 dark:text-white;
|
@apply dark:bg-slate-800! dark:text-white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.simple-keyboard-control .hg-button {
|
.simple-keyboard-control .hg-button {
|
||||||
|
@ -170,25 +308,21 @@ video::-webkit-media-controls {
|
||||||
}
|
}
|
||||||
|
|
||||||
.hg-button.hg-standardBtn[data-skbtn="Space"] {
|
.hg-button.hg-standardBtn[data-skbtn="Space"] {
|
||||||
@apply md:!w-[350px];
|
@apply md:w-[350px]!;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hg-theme-default .hg-row .combination-key {
|
.hg-theme-default .hg-row .combination-key {
|
||||||
@apply inline-flex !h-auto !w-auto flex-grow-0 py-1 text-xs;
|
@apply inline-flex h-auto! w-auto! grow-0 py-1 text-xs;
|
||||||
}
|
|
||||||
|
|
||||||
.hg-theme-default .hg-row:has(.combination-key) {
|
|
||||||
/*margin-bottom: 100px !important;*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.hg-theme-default .hg-row .hg-button-container,
|
.hg-theme-default .hg-row .hg-button-container,
|
||||||
.hg-theme-default .hg-row .hg-button:not(:last-child) {
|
.hg-theme-default .hg-row .hg-button:not(:last-child) {
|
||||||
@apply !mr-[2px] md:!mr-[5px];
|
@apply mr-[2px]! md:mr-[5px]!;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Hide the scrollbar by setting the scrollbar color to the background color */
|
/* Hide the scrollbar by setting the scrollbar color to the background color */
|
||||||
.xterm .xterm-viewport {
|
.xterm .xterm-viewport {
|
||||||
scrollbar-color: theme("colors.gray.900") #002b36;
|
scrollbar-color: var(--color-gray-900) #002b36;
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
|
|
||||||
|
interface 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,15 @@
|
||||||
|
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>;
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue