Compare commits

..

10 Commits

Author SHA1 Message Date
Scai 367f3c0288
Merge 57fbee1490 into 66fbda864a 2025-05-19 22:30:50 -07:00
Aveline 66fbda864a
chore: reset usb after updating config (#482)
* fix(usbgadget): do not panic if a change isn't found

* chore(usbgadget): rebind usb after updating config
2025-05-20 01:29:16 +02:00
Aveline a0f6d01465
fix(usbgadget): do not panic if a change isn't found (#481)
* fix(usbgadget): do not panic if a change isn't found

* chore(usbgadget): rebind usb after updating config
2025-05-20 00:34:32 +02:00
Siyuan Miao b4dd4961fc fix: jetkvm_app path is now bin/jetkvm_app 2025-05-19 23:59:10 +02:00
Aveline eeb103adf9
fix: configFS might not be mounted if the directory exists (#479) 2025-05-19 23:59:02 +02:00
Siyuan Miao 8cf6b40dc3 build: set up golang (smoketest) 2025-05-19 23:25:30 +02:00
Siyuan Miao c6b05d4abe build: add device-tests 2025-05-19 23:23:38 +02:00
Siyuan Miao 51814dcc5e fix: add missing files for building unit tests 2025-05-19 23:05:12 +02:00
Aveline 5ba08de566
fix: unit test not returning error when test fails
* fix: unit test not returning error when test fails

* chore: add unit test to smoketest.yml

* fix: make linter happy
2025-05-19 22:51:11 +02:00
Adam Shiervani 3f320e50f7
refactor: remove scroll sensitivity functionality and clean up related code (#477)
- Removed scroll sensitivity state and associated functions from jsonrpc.go and WebRTCVideo component.
- Cleaned up device settings store by eliminating unused scroll sensitivity logic.
- Updated mouse settings route to reflect the removal of scroll sensitivity feature.
- Simplified mouse wheel event handling in WebRTCVideo component.
2025-05-19 22:44:53 +02:00
18 changed files with 357 additions and 1050 deletions

View File

@ -35,6 +35,9 @@ jobs:
- name: Run tests - name: Run tests
run: | run: |
go test ./... -json > testreport.json go test ./... -json > testreport.json
- name: Make test cases
run: |
make build_dev_test
- name: Golang Test Report - name: Golang Test Report
uses: becheran/go-testreport@v0.3.2 uses: becheran/go-testreport@v0.3.2
with: with:
@ -43,4 +46,6 @@ jobs:
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

View File

@ -72,18 +72,51 @@ jobs:
- name: Run tests - name: Run tests
run: | run: |
set -e set -e
make build_dev_test
echo "+ Copying device-tests.tar.gz to remote host" echo "+ Copying device-tests.tar.gz to remote host"
ssh jkci "cat > /userdata/jetkvm/device-tests.tar.gz" < device-tests.tar.gz ssh jkci "cat > /tmp/device-tests.tar.gz" < device-tests.tar.gz
echo "+ Running go tests" echo "+ Running go tests"
ssh jkci "cd /userdata/jetkvm && tar zxvf device-tests.tar.gz && ./run_all_tests -json" ssh jkci ash << 'EOF'
set -e
TMP_DIR=$(mktemp -d)
cd ${TMP_DIR}
tar zxf /tmp/device-tests.tar.gz
./gotestsum --format=testdox \
--jsonfile=/tmp/device-tests.json \
--post-run-command 'sh -c "echo $TESTS_FAILED > /tmp/device-tests.failed"' \
--raw-command -- ./run_all_tests -json
GOTESTSUM_EXIT_CODE=$?
if [ $GOTESTSUM_EXIT_CODE -ne 0 ]; then
echo "❌ Tests failed (exit code: $GOTESTSUM_EXIT_CODE)"
rm -rf ${TMP_DIR} /tmp/device-tests.tar.gz
exit 1
fi
TESTS_FAILED=$(cat /tmp/device-tests.failed)
if [ "$TESTS_FAILED" -ne 0 ]; then
echo "❌ Tests failed $TESTS_FAILED tests failed"
rm -rf ${TMP_DIR} /tmp/device-tests.tar.gz
exit 1
fi
echo "✅ Tests passed"
rm -rf ${TMP_DIR} /tmp/device-tests.tar.gz
EOF
ssh jkci "cat /tmp/device-tests.json" > device-tests.json
- name: Set up Golang
uses: actions/setup-go@v4
with:
go-version: "1.24.0"
- name: Golang Test Report
uses: becheran/go-testreport@v0.3.2
with:
input: "device-tests.json"
- name: Deploy application - 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
@ -117,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

View File

@ -15,6 +15,9 @@ 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) TEST_DIRS := $(shell find . -name "*_test.go" -type f -exec dirname {} \; | sort -u)
hash_resource: hash_resource:
@ -22,31 +25,35 @@ hash_resource:
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)" -o $(BIN_DIR)/jetkvm_app cmd/main.go
build_test2json: build_test2json:
GOOS=linux GOARCH=arm GOARM=7 go build -o bin/test2json cmd/test2json $(GO_CMD) build -o $(BIN_DIR)/test2json cmd/test2json
build_dev_test: build_test2json build_gotestsum:
@echo "Building gotestsum..."
$(GO_CMD) install gotest.tools/gotestsum@latest
cp $(shell $(GO_CMD) env GOPATH)/bin/linux_arm/gotestsum $(BIN_DIR)/gotestsum
build_dev_test: build_test2json build_gotestsum
# collect all directories that contain tests # collect all directories that contain tests
@echo "Building tests for devices ..." @echo "Building tests for devices ..."
@rm -rf bin/tests && mkdir -p bin/tests @rm -rf $(BIN_DIR)/tests && mkdir -p $(BIN_DIR)/tests
@cat resource/dev_test.sh > bin/tests/run_all_tests @cat resource/dev_test.sh > $(BIN_DIR)/tests/run_all_tests
@for test in $(TEST_DIRS); do \ @for test in $(TEST_DIRS); do \
test_pkg_name=$$(echo $$test | sed 's/^.\///g'); \ test_pkg_name=$$(echo $$test | sed 's/^.\///g'); \
test_pkg_full_name=$(KVM_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; \ test_filename=$$(echo $$test_pkg_name | sed 's/\//__/g')_test; \
GOOS=linux GOARCH=arm GOARM=7 \ $(GO_CMD) test -v \
go test -v \
-ldflags="$(GO_LDFLAGS) -X $(KVM_PKG_NAME).builtAppVersion=$(VERSION_DEV)" \ -ldflags="$(GO_LDFLAGS) -X $(KVM_PKG_NAME).builtAppVersion=$(VERSION_DEV)" \
-c -o bin/tests/$$test_filename $$test; \ -c -o $(BIN_DIR)/tests/$$test_filename $$test; \
echo "runTest ./$$test_filename $$test_pkg_full_name" >> bin/tests/run_all_tests; \ echo "runTest ./$$test_filename $$test_pkg_full_name" >> $(BIN_DIR)/tests/run_all_tests; \
done; \ done; \
chmod +x bin/tests/run_all_tests; \ chmod +x $(BIN_DIR)/tests/run_all_tests; \
cp bin/test2json bin/tests/; \ cp $(BIN_DIR)/test2json $(BIN_DIR)/tests/ && chmod +x $(BIN_DIR)/tests/test2json; \
chmod +x bin/tests/test2json; \ cp $(BIN_DIR)/gotestsum $(BIN_DIR)/tests/ && chmod +x $(BIN_DIR)/tests/gotestsum; \
tar czfv device-tests.tar.gz -C bin/tests . 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
@ -59,7 +66,7 @@ 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)" -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 \

View File

@ -27,7 +27,6 @@ show_help() {
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 Run go tests"
echo " --run-go-tests-only Run go tests and exit" echo " --run-go-tests-only Run go tests and exit"
echo " --run-go-tests-json Run go tests and output JSON"
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
@ -43,7 +42,6 @@ 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=false
RUN_GO_TESTS_JSON=false
RUN_GO_TESTS_ONLY=false RUN_GO_TESTS_ONLY=false
# Parse command line arguments # Parse command line arguments
@ -69,11 +67,6 @@ while [[ $# -gt 0 ]]; do
RUN_GO_TESTS=true RUN_GO_TESTS=true
shift shift
;; ;;
--run-go-tests-json)
RUN_GO_TESTS_JSON=true
RUN_GO_TESTS=true
shift
;;
--run-go-tests-only) --run-go-tests-only)
RUN_GO_TESTS_ONLY=true RUN_GO_TESTS_ONLY=true
RUN_GO_TESTS=true RUN_GO_TESTS=true
@ -109,18 +102,35 @@ if [ "$RUN_GO_TESTS" = true ]; then
make build_dev_test make build_dev_test
msg_info "▶ Copying device-tests.tar.gz to remote host" msg_info "▶ Copying device-tests.tar.gz to remote host"
ssh "${REMOTE_USER}@${REMOTE_HOST}" "cat > ${REMOTE_PATH}/device-tests.tar.gz" < device-tests.tar.gz ssh "${REMOTE_USER}@${REMOTE_HOST}" "cat > /tmp/device-tests.tar.gz" < device-tests.tar.gz
msg_info "▶ Running go tests" msg_info "▶ Running go tests"
TEST_ARGS="" ssh "${REMOTE_USER}@${REMOTE_HOST}" ash << 'EOF'
if [ "$RUN_GO_TESTS_JSON" = true ]; then
TEST_ARGS="-json"
fi
ssh "${REMOTE_USER}@${REMOTE_HOST}" ash << EOF
set -e set -e
cd ${REMOTE_PATH} TMP_DIR=$(mktemp -d)
tar zxvf device-tests.tar.gz cd ${TMP_DIR}
PION_LOG_TRACE=all ./run_all_tests $TEST_ARGS 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 EOF
if [ "$RUN_GO_TESTS_ONLY" = true ]; then if [ "$RUN_GO_TESTS_ONLY" = true ]; then

10
go.mod
View File

@ -30,9 +30,9 @@ require (
github.com/stretchr/testify v1.10.0 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.38.0
golang.org/x/net v0.38.0 golang.org/x/net v0.40.0
golang.org/x/sys v0.32.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
@ -52,6 +52,7 @@ require (
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/google/go-cmp v0.7.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/compress v1.17.11 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/klauspost/cpuid/v2 v2.2.10 // indirect
@ -78,13 +79,14 @@ require (
github.com/pion/turn/v4 v4.0.0 // indirect github.com/pion/turn/v4 v4.0.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // 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/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.15.0 // indirect
golang.org/x/oauth2 v0.24.0 // indirect golang.org/x/oauth2 v0.24.0 // indirect
golang.org/x/text v0.23.0 // indirect golang.org/x/text v0.25.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

24
go.sum
View File

@ -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=
@ -147,8 +147,8 @@ github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0leargg
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/psanford/httpreadat v0.1.0 h1:VleW1HS2zO7/4c7c7zNl33fO6oYACSagjJIyMIwZLUE= github.com/psanford/httpreadat v0.1.0 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=
@ -181,10 +181,10 @@ go.bug.st/serial v1.6.2 h1:kn9LRX3sdm+WxWKufMlIRndwGfPWsH1/9lCWXQCasq8=
go.bug.st/serial v1.6.2/go.mod h1:UABfsluHAiaNI+La2iESysd9Vetq7VRdpxvjx7CmmOE= go.bug.st/serial v1.6.2/go.mod h1:UABfsluHAiaNI+La2iESysd9Vetq7VRdpxvjx7CmmOE=
golang.org/x/arch v0.15.0 h1:QtOrQd0bTUnhNVNndMpLHNWrDmYzZ2KDqSrEymqInZw= golang.org/x/arch v0.15.0 h1:QtOrQd0bTUnhNVNndMpLHNWrDmYzZ2KDqSrEymqInZw=
golang.org/x/arch v0.15.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE= golang.org/x/arch v0.15.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= golang.org/x/oauth2 v0.24.0 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/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -194,10 +194,10 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.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.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6 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=

View File

@ -48,7 +48,7 @@ var FileStateString = map[FileState]string{
FileStateFileContentMatch: "FILE_CONTENT_MATCH", FileStateFileContentMatch: "FILE_CONTENT_MATCH",
FileStateFileWrite: "FILE_WRITE", FileStateFileWrite: "FILE_WRITE",
FileStateMounted: "MOUNTED", FileStateMounted: "MOUNTED",
FileStateMountedConfigFS: "CONFIGFS_MOUNT", FileStateMountedConfigFS: "CONFIGFS_MOUNTED",
FileStateSymlink: "SYMLINK", FileStateSymlink: "SYMLINK",
FileStateSymlinkInOrderConfigFS: "SYMLINK_IN_ORDER_CONFIGFS", FileStateSymlinkInOrderConfigFS: "SYMLINK_IN_ORDER_CONFIGFS",
FileStateTouch: "TOUCH", FileStateTouch: "TOUCH",
@ -155,6 +155,10 @@ func (f *RequestedFileChange) String() string {
s = fmt.Sprintf("unknown expected state %d for %s", f.ExpectedState, f.Path) 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 return s
} }

View File

@ -48,6 +48,11 @@ func (c *ChangeSetResolver) doResolveChanges(initial bool) error {
for _, key := range c.orderedChanges { for _, key := range c.orderedChanges {
change := c.changesMap[key.(string)] change := c.changesMap[key.(string)]
if change == nil {
c.l.Error().Str("key", key.(string)).Msg("fileChange not found")
continue
}
if !initial { if !initial {
change.ResetActionResolution() change.ResetActionResolution()
} }
@ -123,9 +128,13 @@ func (c *ChangeSetResolver) applyChanges() error {
err := c.changeset.applyChange(change) err := c.changeset.applyChange(change)
if err != nil { if err != nil {
if change.IgnoreErrors {
c.l.Warn().Str("change", change.String()).Err(err).Msg("ignoring error")
} else {
return err return err
} }
} }
}
return nil return nil
} }

View File

@ -29,7 +29,7 @@ func checkIfSymlinksInOrder(fc *FileChange, logger *zerolog.Logger) (FileState,
} }
l := logger.With().Str("path", fc.Path).Logger() l := logger.With().Str("path", fc.Path).Logger()
if fc.ParamSymlinks == nil || len(fc.ParamSymlinks) == 0 { if len(fc.ParamSymlinks) == 0 {
return FileStateUnknown, fmt.Errorf("no symlinks to check") return FileStateUnknown, fmt.Errorf("no symlinks to check")
} }

View File

@ -2,7 +2,6 @@ package usbgadget
import ( import (
"fmt" "fmt"
"os"
"os/exec" "os/exec"
) )
@ -158,20 +157,10 @@ func (u *UsbGadget) OverrideGadgetConfig(itemKey string, itemAttr string, value
} }
func mountConfigFS(path string) error { func mountConfigFS(path string) error {
_, err := os.Stat(path) 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", path).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
} }
@ -188,12 +177,7 @@ func (u *UsbGadget) Init() error {
u.udc = udcs[0] u.udc = udcs[0]
err := u.WithTransaction(func() error { err := u.configureUsbGadget(false)
u.tx.MountConfigFS()
u.tx.CreateConfigPath()
u.tx.WriteGadgetConfig()
return nil
})
if err != nil { if err != nil {
return u.logError("unable to initialize USB stack", err) return u.logError("unable to initialize USB stack", err)
} }
@ -207,13 +191,22 @@ func (u *UsbGadget) UpdateGadgetConfig() error {
u.loadGadgetConfig() u.loadGadgetConfig()
err := u.WithTransaction(func() error { err := u.configureUsbGadget(true)
u.tx.WriteGadgetConfig()
return nil
})
if err != nil { if err != nil {
return u.logError("unable to update gadget config", err) return u.logError("unable to update gadget config", err)
} }
return nil return nil
} }
func (u *UsbGadget) configureUsbGadget(resetUsb bool) error {
return u.WithTransaction(func() error {
u.tx.MountConfigFS()
u.tx.CreateConfigPath()
u.tx.WriteGadgetConfig()
if resetUsb {
u.tx.RebindUsb(true)
}
return nil
})
}

View File

@ -81,11 +81,12 @@ func (tx *UsbGadgetTransaction) addFileChange(component string, change Requested
return key return key
} }
func (tx *UsbGadgetTransaction) mkdirAll(component string, path string, description string) string { func (tx *UsbGadgetTransaction) mkdirAll(component string, path string, description string, deps []string) string {
return tx.addFileChange(component, RequestedFileChange{ return tx.addFileChange(component, RequestedFileChange{
Path: path, Path: path,
ExpectedState: FileStateDirectory, ExpectedState: FileStateDirectory,
Description: description, Description: description,
DependsOn: deps,
}) })
} }
@ -131,14 +132,25 @@ func (tx *UsbGadgetTransaction) MountConfigFS() {
} }
func (tx *UsbGadgetTransaction) CreateConfigPath() { func (tx *UsbGadgetTransaction) CreateConfigPath() {
tx.mkdirAll("gadget", tx.configC1Path, "create config path") tx.mkdirAll(
"gadget",
tx.configC1Path,
"create config path",
[]string{configFSPath},
)
} }
func (tx *UsbGadgetTransaction) WriteGadgetConfig() { func (tx *UsbGadgetTransaction) WriteGadgetConfig() {
// create kvm gadget path // create kvm gadget path
tx.mkdirAll("gadget", tx.kvmGadgetPath, "create kvm gadget path") tx.mkdirAll(
"gadget",
tx.kvmGadgetPath,
"create kvm gadget path",
[]string{tx.configC1Path},
)
deps := make([]string, 0) deps := make([]string, 0)
deps = append(deps, tx.kvmGadgetPath)
for _, val := range tx.orderedConfigItems { for _, val := range tx.orderedConfigItems {
key := val.key key := val.key
@ -188,7 +200,10 @@ func (tx *UsbGadgetTransaction) writeGadgetItemConfig(item gadgetConfigItem, dep
files = append(files, deps...) files = append(files, deps...)
gadgetItemPath := joinPath(tx.kvmGadgetPath, item.path) gadgetItemPath := joinPath(tx.kvmGadgetPath, item.path)
files = append(files, tx.mkdirAll(component, gadgetItemPath, "create gadget item directory")) if gadgetItemPath != tx.kvmGadgetPath {
gadgetItemDir := tx.mkdirAll(component, gadgetItemPath, "create gadget item directory", files)
files = append(files, gadgetItemDir)
}
beforeChange := make([]string, 0) beforeChange := make([]string, 0)
disableGadgetItemKey := fmt.Sprintf("disable-%s", item.device) disableGadgetItemKey := fmt.Sprintf("disable-%s", item.device)
@ -231,7 +246,10 @@ func (tx *UsbGadgetTransaction) writeGadgetItemConfig(item gadgetConfigItem, dep
// create config directory if configAttrs are set // create config directory if configAttrs are set
if len(item.configAttrs) > 0 { if len(item.configAttrs) > 0 {
configItemPath := joinPath(tx.configC1Path, item.configPath) configItemPath := joinPath(tx.configC1Path, item.configPath)
tx.mkdirAll(component, configItemPath, "create config item directory") if configItemPath != tx.configC1Path {
configItemDir := tx.mkdirAll(component, configItemPath, "create config item directory", files)
files = append(files, configItemDir)
}
files = append(files, tx.writeGadgetAttrs( files = append(files, tx.writeGadgetAttrs(
configItemPath, configItemPath,
item.configAttrs, item.configAttrs,
@ -301,6 +319,7 @@ func (tx *UsbGadgetTransaction) WriteUDC() {
// bound the gadget to a UDC (USB Device Controller) // bound the gadget to a UDC (USB Device Controller)
path := path.Join(tx.kvmGadgetPath, "UDC") path := path.Join(tx.kvmGadgetPath, "UDC")
tx.addFileChange("udc", RequestedFileChange{ tx.addFileChange("udc", RequestedFileChange{
Key: "udc",
Path: path, Path: path,
ExpectedState: FileStateFileContentMatch, ExpectedState: FileStateFileContentMatch,
ExpectedContent: []byte(tx.udc), ExpectedContent: []byte(tx.udc),
@ -316,6 +335,8 @@ func (tx *UsbGadgetTransaction) RebindUsb(ignoreUnbindError bool) {
ExpectedState: FileStateFileWrite, ExpectedState: FileStateFileWrite,
ExpectedContent: []byte(tx.udc), ExpectedContent: []byte(tx.udc),
Description: "unbind UDC", Description: "unbind UDC",
DependsOn: []string{"udc"},
IgnoreErrors: ignoreUnbindError,
}) })
// bind the gadget to the UDC // bind the gadget to the UDC
tx.addFileChange("udc", RequestedFileChange{ tx.addFileChange("udc", RequestedFileChange{
@ -324,6 +345,5 @@ func (tx *UsbGadgetTransaction) RebindUsb(ignoreUnbindError bool) {
ExpectedContent: []byte(tx.udc), ExpectedContent: []byte(tx.udc),
Description: "bind UDC", Description: "bind UDC",
DependsOn: []string{path.Join(tx.dwc3Path, "unbind")}, DependsOn: []string{path.Join(tx.dwc3Path, "unbind")},
IgnoreErrors: ignoreUnbindError,
}) })
} }

View File

@ -877,17 +877,6 @@ func rpcSetCloudUrl(apiUrl string, appUrl string) error {
return nil return nil
} }
var currentScrollSensitivity string = "default"
func rpcGetScrollSensitivity() (string, error) {
return currentScrollSensitivity, nil
}
func rpcSetScrollSensitivity(sensitivity string) error {
currentScrollSensitivity = sensitivity
return nil
}
func getKeyboardMacros() (interface{}, error) { func getKeyboardMacros() (interface{}, error) {
macros := make([]KeyboardMacro, len(config.KeyboardMacros)) macros := make([]KeyboardMacro, len(config.KeyboardMacros))
copy(macros, config.KeyboardMacros) copy(macros, config.KeyboardMacros)
@ -1053,8 +1042,6 @@ 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},
"setScrollSensitivity": {Func: rpcSetScrollSensitivity, Params: []string{"sensitivity"}},
"getKeyboardMacros": {Func: getKeyboardMacros}, "getKeyboardMacros": {Func: getKeyboardMacros},
"setKeyboardMacros": {Func: setKeyboardMacros, Params: []string{"params"}}, "setKeyboardMacros": {Func: setKeyboardMacros, Params: []string{"params"}},
} }

View File

@ -29,8 +29,6 @@ runTest() {
function exit_with_code() { function exit_with_code() {
if [ $EXIT_CODE -ne 0 ]; then if [ $EXIT_CODE -ne 0 ]; then
printf "\e[0;31m❌ Test failed\e[0m\n" printf "\e[0;31m❌ Test failed\e[0m\n"
else
printf "\e[0;32m✅ All tests passed\e[0m\n"
fi fi
exit $EXIT_CODE exit $EXIT_CODE

891
ui/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useResizeObserver } from "usehooks-ts"; import { useResizeObserver } from "usehooks-ts";
import { import {
useDeviceSettingsStore,
useHidStore, useHidStore,
useMouseStore, useMouseStore,
useRTCStore, useRTCStore,
@ -61,7 +60,6 @@ export default function WebRTCVideo() {
useHidStore(); useHidStore();
// Misc states and hooks // Misc states and hooks
const [blockWheelEvent, setBlockWheelEvent] = useState(false);
const disableVideoFocusTrap = useUiStore(state => state.disableVideoFocusTrap); const disableVideoFocusTrap = useUiStore(state => state.disableVideoFocusTrap);
const [send] = useJsonRpc(); const [send] = useJsonRpc();
@ -248,17 +246,8 @@ export default function WebRTCVideo() {
], ],
); );
const trackpadSensitivity = useDeviceSettingsStore(state => state.trackpadSensitivity);
const mouseSensitivity = useDeviceSettingsStore(state => state.mouseSensitivity);
const clampMin = useDeviceSettingsStore(state => state.clampMin);
const clampMax = useDeviceSettingsStore(state => state.clampMax);
const blockDelay = useDeviceSettingsStore(state => state.blockDelay);
const trackpadThreshold = useDeviceSettingsStore(state => state.trackpadThreshold);
const mouseWheelHandler = useCallback( const mouseWheelHandler = useCallback(
(e: WheelEvent) => { (e: WheelEvent) => {
if (blockWheelEvent) return;
// Determine if the wheel event is an accel scroll value // Determine if the wheel event is an accel scroll value
const isAccel = Math.abs(e.deltaY) >= 100; const isAccel = Math.abs(e.deltaY) >= 100;
@ -266,7 +255,7 @@ export default function WebRTCVideo() {
const accelScrollValue = e.deltaY / 100; const accelScrollValue = e.deltaY / 100;
// Calculate the no accel scroll value // Calculate the no accel scroll value
const noAccelScrollValue = e.deltaY > 0 ? 1 : (e.deltaY < 0 ? -1 : 0); const noAccelScrollValue = e.deltaY > 0 ? 1 : e.deltaY < 0 ? -1 : 0;
// Get scroll value // Get scroll value
const scrollValue = isAccel ? accelScrollValue : noAccelScrollValue; const scrollValue = isAccel ? accelScrollValue : noAccelScrollValue;
@ -278,21 +267,8 @@ export default function WebRTCVideo() {
const invertedScrollValue = -clampedScrollValue; const invertedScrollValue = -clampedScrollValue;
send("wheelReport", { wheelY: invertedScrollValue }); send("wheelReport", { wheelY: invertedScrollValue });
// Apply blocking delay
setBlockWheelEvent(true);
setTimeout(() => setBlockWheelEvent(false), blockDelay);
}, },
[ [send],
blockDelay,
blockWheelEvent,
clampMax,
clampMin,
mouseSensitivity,
send,
trackpadSensitivity,
trackpadThreshold,
],
); );
const resetMousePosition = useCallback(() => { const resetMousePosition = useCallback(() => {
@ -351,11 +327,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)
@ -716,7 +688,7 @@ export default function WebRTCVideo() {
disablePictureInPicture disablePictureInPicture
controlsList="nofullscreen" controlsList="nofullscreen"
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", "z-30 max-h-full min-h-[384px] max-w-full min-w-[512px] bg-black/50 object-contain transition-all duration-1000",
{ {
"cursor-none": settings.isCursorHidden, "cursor-none": settings.isCursorHidden,
"opacity-0": "opacity-0":
@ -732,7 +704,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" 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} />

View File

@ -321,8 +321,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,
@ -350,67 +349,6 @@ export interface DeviceSettingsState {
setScrollSensitivity: (sensitivity: DeviceSettingsState["scrollSensitivity"]) => void; 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;

View File

@ -1,23 +1,20 @@
import { CheckCircleIcon } from "@heroicons/react/16/solid"; import { CheckCircleIcon } from "@heroicons/react/16/solid";
import { useCallback, useEffect, useState } from "react"; import { useEffect, useState } from "react";
import MouseIcon from "@/assets/mouse-icon.svg"; import MouseIcon from "@/assets/mouse-icon.svg";
import PointingFinger from "@/assets/pointing-finger.svg"; import PointingFinger from "@/assets/pointing-finger.svg";
import { GridCard } from "@/components/Card"; import { GridCard } from "@/components/Card";
import { Checkbox } from "@/components/Checkbox"; import { Checkbox } from "@/components/Checkbox";
import { useDeviceSettingsStore, useSettingsStore } from "@/hooks/stores"; import { useSettingsStore } from "@/hooks/stores";
import { useJsonRpc } from "@/hooks/useJsonRpc"; import { useJsonRpc } from "@/hooks/useJsonRpc";
import notifications from "@/notifications"; import notifications from "@/notifications";
import { SettingsPageHeader } from "@components/SettingsPageheader"; import { SettingsPageHeader } from "@components/SettingsPageheader";
import { FeatureFlag } from "../components/FeatureFlag";
import { SelectMenuBasic } from "../components/SelectMenuBasic";
import { useFeatureFlag } from "../hooks/useFeatureFlag"; import { useFeatureFlag } from "../hooks/useFeatureFlag";
import { cx } from "../cva.config";
import { SettingsItem } from "./devices.$id.settings"; import { SettingsItem } from "./devices.$id.settings";
type ScrollSensitivity = "low" | "default" | "high";
export default function SettingsKeyboardMouseRoute() { export default function SettingsKeyboardMouseRoute() {
const hideCursor = useSettingsStore(state => state.isCursorHidden); const hideCursor = useSettingsStore(state => state.isCursorHidden);
const setHideCursor = useSettingsStore(state => state.setCursorVisibility); const setHideCursor = useSettingsStore(state => state.setCursorVisibility);
@ -25,11 +22,6 @@ export default function SettingsKeyboardMouseRoute() {
const mouseMode = useSettingsStore(state => state.mouseMode); const mouseMode = useSettingsStore(state => state.mouseMode);
const setMouseMode = useSettingsStore(state => state.setMouseMode); const setMouseMode = useSettingsStore(state => state.setMouseMode);
const scrollSensitivity = useDeviceSettingsStore(state => state.scrollSensitivity);
const setScrollSensitivity = useDeviceSettingsStore(
state => state.setScrollSensitivity,
);
const { isEnabled: isScrollSensitivityEnabled } = useFeatureFlag("0.3.8"); const { isEnabled: isScrollSensitivityEnabled } = useFeatureFlag("0.3.8");
const [jiggler, setJiggler] = useState(false); const [jiggler, setJiggler] = useState(false);
@ -41,14 +33,7 @@ export default function SettingsKeyboardMouseRoute() {
if ("error" in resp) return; if ("error" in resp) return;
setJiggler(resp.result as boolean); setJiggler(resp.result as boolean);
}); });
}, [isScrollSensitivityEnabled, send]);
if (isScrollSensitivityEnabled) {
send("getScrollSensitivity", {}, resp => {
if ("error" in resp) return;
setScrollSensitivity(resp.result as ScrollSensitivity);
});
}
}, [isScrollSensitivityEnabled, send, setScrollSensitivity]);
const handleJigglerChange = (enabled: boolean) => { const handleJigglerChange = (enabled: boolean) => {
send("setJigglerState", { enabled }, resp => { send("setJigglerState", { enabled }, resp => {
@ -62,22 +47,6 @@ export default function SettingsKeyboardMouseRoute() {
}); });
}; };
const onScrollSensitivityChange = useCallback(
(e: React.ChangeEvent<HTMLSelectElement>) => {
const sensitivity = e.target.value as ScrollSensitivity;
send("setScrollSensitivity", { sensitivity }, resp => {
if ("error" in resp) {
notifications.error(
`Failed to set scroll sensitivity: ${resp.error.data || "Unknown error"}`,
);
}
notifications.success("Scroll sensitivity set successfully");
setScrollSensitivity(sensitivity);
});
},
[send, setScrollSensitivity],
);
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<SettingsPageHeader <SettingsPageHeader
@ -96,28 +65,6 @@ export default function SettingsKeyboardMouseRoute() {
/> />
</SettingsItem> </SettingsItem>
<FeatureFlag minAppVersion="0.3.8" name="Scroll Sensitivity">
<SettingsItem
title="Scroll Sensitivity"
description="Adjust the scroll sensitivity"
>
<SelectMenuBasic
size="SM"
label=""
fullWidth
value={scrollSensitivity}
onChange={onScrollSensitivityChange}
options={
[
{ label: "Low", value: "low" },
{ label: "Default", value: "default" },
{ label: "High", value: "high" },
] as { label: string; value: ScrollSensitivity }[]
}
/>
</SettingsItem>
</FeatureFlag>
<SettingsItem <SettingsItem
title="Jiggler" title="Jiggler"
description="Simulate movement of a computer mouse. Prevents sleep mode, standby mode or the screensaver from activating" description="Simulate movement of a computer mouse. Prevents sleep mode, standby mode or the screensaver from activating"
@ -131,17 +78,19 @@ export default function SettingsKeyboardMouseRoute() {
<SettingsItem title="Modes" description="Choose the mouse input mode" /> <SettingsItem title="Modes" description="Choose the mouse input mode" />
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<button <button
className="block group grow" className="group block grow"
onClick={() => { setMouseMode("absolute"); }} onClick={() => {
setMouseMode("absolute");
}}
> >
<GridCard> <GridCard>
<div className="flex items-center px-4 py-3 group gap-x-4"> <div className="group flex w-full items-center gap-x-4 px-4 py-3">
<img <img
className="w-6 shrink-0 dark:invert" className="w-6 shrink-0 dark:invert"
src={PointingFinger} src={PointingFinger}
alt="Finger touching a screen" alt="Finger touching a screen"
/> />
<div className="flex items-center justify-between grow"> <div className="flex grow items-center justify-between">
<div className="text-left"> <div className="text-left">
<h3 className="text-sm font-semibold text-black dark:text-white"> <h3 className="text-sm font-semibold text-black dark:text-white">
Absolute Absolute
@ -150,32 +99,44 @@ export default function SettingsKeyboardMouseRoute() {
Most convenient Most convenient
</p> </p>
</div> </div>
{mouseMode === "absolute" && ( <CheckCircleIcon
<CheckCircleIcon className="w-4 h-4 text-blue-700 dark:text-blue-500" /> className={cx(
"h-4 w-4 text-blue-700 opacity-0 transition dark:text-blue-500",
{ "opacity-100": mouseMode === "absolute" },
)} )}
/>
</div> </div>
</div> </div>
</GridCard> </GridCard>
</button> </button>
<button <button
className="block group grow" className="group block grow"
onClick={() => { setMouseMode("relative"); }} onClick={() => {
setMouseMode("relative");
}}
> >
<GridCard> <GridCard>
<div className="flex items-center px-4 py-3 gap-x-4"> <div className="flex w-full items-center gap-x-4 px-4 py-3">
<img className="w-6 shrink-0 dark:invert" src={MouseIcon} alt="Mouse icon" /> <img
<div className="flex items-center justify-between grow"> className="w-6 shrink-0 dark:invert"
src={MouseIcon}
alt="Mouse icon"
/>
<div className="flex grow items-center justify-between">
<div className="text-left"> <div className="text-left">
<h3 className="text-sm font-semibold text-black dark:text-white"> <h3 className="text-sm font-semibold text-black dark:text-white">
Relative Relative
</h3> </h3>
<p className="text-xs leading-none text-slate-800 dark:text-slate-300"> <p className="text-xs leading-none text-slate-800 dark:text-slate-300">
Most Compatible (Beta) Most Compatible
</p> </p>
</div> </div>
{mouseMode === "relative" && ( <CheckCircleIcon
<CheckCircleIcon className="w-4 h-4 text-blue-700 dark:text-blue-500" /> className={cx(
"h-4 w-4 text-blue-700 opacity-0 transition dark:text-blue-500",
{ "opacity-100": mouseMode === "relative" },
)} )}
/>
</div> </div>
</div> </div>
</GridCard> </GridCard>

View File

@ -18,11 +18,9 @@ import useWebSocket from "react-use-websocket";
import { cx } from "@/cva.config"; import { cx } from "@/cva.config";
import { import {
DeviceSettingsState,
HidState, HidState,
NetworkState, NetworkState,
UpdateState, UpdateState,
useDeviceSettingsStore,
useDeviceStore, useDeviceStore,
useHidStore, useHidStore,
useMountMediaStore, useMountMediaStore,
@ -714,21 +712,6 @@ export default function KvmIdRoute() {
}); });
}, [appVersion, send, setAppVersion, setSystemVersion]); }, [appVersion, send, setAppVersion, setSystemVersion]);
const setScrollSensitivity = useDeviceSettingsStore(
state => state.setScrollSensitivity,
);
// Initialize device settings
useEffect(
function initializeDeviceSettings() {
send("getScrollSensitivity", {}, resp => {
if ("error" in resp) return;
setScrollSensitivity(resp.result as DeviceSettingsState["scrollSensitivity"]);
});
},
[send, setScrollSensitivity],
);
const ConnectionStatusElement = useMemo(() => { const ConnectionStatusElement = useMemo(() => {
const hasConnectionFailed = const hasConnectionFailed =
connectionFailed || ["failed", "closed"].includes(peerConnectionState || ""); connectionFailed || ["failed", "closed"].includes(peerConnectionState || "");