mirror of https://github.com/jetkvm/kvm.git
Compare commits
14 Commits
740a44fe86
...
77a360397e
| Author | SHA1 | Date |
|---|---|---|
|
|
77a360397e | |
|
|
63aa940f42 | |
|
|
043ef9ddfc | |
|
|
437f0b854a | |
|
|
a45d55123c | |
|
|
213e750e04 | |
|
|
6dcb0286e3 | |
|
|
74ccca0b1a | |
|
|
0ad435475b | |
|
|
23bf3978fa | |
|
|
531f6bf65b | |
|
|
25e476e715 | |
|
|
8dd004ab54 | |
|
|
9f27a5d5c3 |
|
|
@ -43,7 +43,7 @@ jobs:
|
|||
cache: "npm"
|
||||
cache-dependency-path: "**/package-lock.json"
|
||||
- name: Set up Golang
|
||||
uses: actions/setup-go@v6.0.0
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "^1.25.1"
|
||||
- name: Build frontend
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ jobs:
|
|||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: oldstable
|
||||
- name: Create empty resource directory
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ jobs:
|
|||
EOF
|
||||
ssh jkci "cat /tmp/device-tests.json" > device-tests.json
|
||||
- name: Set up Golang
|
||||
uses: actions/setup-go@v5.5.0
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.24.4"
|
||||
- name: Golang Test Report
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ const (
|
|||
MaxMacrosPerDevice = 25
|
||||
MaxStepsPerMacro = 10
|
||||
MaxKeysPerStep = 10
|
||||
MinStepDelay = 50
|
||||
MinStepDelay = 10
|
||||
MaxStepDelay = 2000
|
||||
)
|
||||
|
||||
|
|
@ -32,6 +32,10 @@ type KeyboardMacroStep struct {
|
|||
Keys []string `json:"keys"`
|
||||
Modifiers []string `json:"modifiers"`
|
||||
Delay int `json:"delay"`
|
||||
// Optional: when set, this step types the given text using the configured keyboard layout.
|
||||
// The delay value is treated as the per-character delay.
|
||||
Text string `json:"text,omitempty"`
|
||||
Wait bool `json:"wait,omitempty"`
|
||||
}
|
||||
|
||||
func (s *KeyboardMacroStep) Validate() error {
|
||||
|
|
|
|||
48
go.mod
48
go.mod
|
|
@ -5,13 +5,14 @@ go 1.24.4
|
|||
require (
|
||||
github.com/Masterminds/semver/v3 v3.4.0
|
||||
github.com/beevik/ntp v1.4.3
|
||||
github.com/coder/websocket v1.8.13
|
||||
github.com/coder/websocket v1.8.14
|
||||
github.com/coreos/go-oidc/v3 v3.15.0
|
||||
github.com/creack/pty v1.1.24
|
||||
github.com/erikdubbelboer/gspt v0.0.0-20210805194459-ce36a5128377
|
||||
github.com/fsnotify/fsnotify v1.9.0
|
||||
github.com/gin-contrib/logger v1.2.6
|
||||
github.com/gin-gonic/gin v1.10.1
|
||||
github.com/go-co-op/gocron/v2 v2.16.5
|
||||
github.com/gin-gonic/gin v1.11.0
|
||||
github.com/go-co-op/gocron/v2 v2.16.6
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/guregu/null/v6 v6.0.0
|
||||
github.com/gwatts/rootcerts v0.0.0-20250901182336-dc5ae18bd79f
|
||||
|
|
@ -19,8 +20,8 @@ require (
|
|||
github.com/pion/mdns/v2 v2.0.7
|
||||
github.com/pion/webrtc/v4 v4.1.4
|
||||
github.com/pojntfx/go-nbd v0.3.2
|
||||
github.com/prometheus/client_golang v1.23.0
|
||||
github.com/prometheus/common v0.66.0
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/prometheus/common v0.66.1
|
||||
github.com/prometheus/procfs v0.17.0
|
||||
github.com/psanford/httpreadat v0.1.0
|
||||
github.com/rs/xid v1.6.0
|
||||
|
|
@ -30,37 +31,32 @@ require (
|
|||
github.com/vearutop/statigz v1.5.0
|
||||
github.com/vishvananda/netlink v1.3.1
|
||||
go.bug.st/serial v1.6.4
|
||||
golang.org/x/crypto v0.41.0
|
||||
golang.org/x/net v0.43.0
|
||||
golang.org/x/sys v0.35.0
|
||||
golang.org/x/crypto v0.42.0
|
||||
golang.org/x/net v0.44.0
|
||||
golang.org/x/sys v0.36.0
|
||||
)
|
||||
|
||||
replace github.com/pojntfx/go-nbd v0.3.2 => github.com/chemhack/go-nbd v0.0.0-20241006125820-59e45f5b1e7b
|
||||
|
||||
require (
|
||||
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bytedance/sonic v1.13.3 // indirect
|
||||
github.com/bytedance/sonic/loader v0.2.4 // indirect
|
||||
github.com/bytedance/sonic v1.14.0 // indirect
|
||||
github.com/bytedance/sonic/loader v0.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.6 // indirect
|
||||
github.com/creack/goselect v0.1.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/erikdubbelboer/gspt v0.0.0-20210805194459-ce36a5128377 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
|
||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.0 // indirect
|
||||
github.com/go-ole/go-ole v1.2.4 // indirect
|
||||
github.com/go-playground/locales v0.14.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.27.0 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect
|
||||
github.com/goccy/go-yaml v1.18.0 // indirect
|
||||
github.com/jonboulle/clockwork v0.5.0 // indirect
|
||||
github.com/jpillora/overseer v1.1.6 // indirect
|
||||
github.com/jpillora/s3 v1.1.4 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
|
|
@ -84,16 +80,22 @@ require (
|
|||
github.com/pion/turn/v4 v4.1.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/quic-go/qpack v0.5.1 // indirect
|
||||
github.com/quic-go/quic-go v0.54.0 // indirect
|
||||
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.3.0 // indirect
|
||||
github.com/vishvananda/netns v0.0.5 // indirect
|
||||
github.com/wlynxg/anet v0.0.5 // indirect
|
||||
golang.org/x/arch v0.18.0 // indirect
|
||||
go.uber.org/mock v0.5.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||
golang.org/x/arch v0.20.0 // indirect
|
||||
golang.org/x/mod v0.27.0 // indirect
|
||||
golang.org/x/oauth2 v0.30.0 // indirect
|
||||
golang.org/x/text v0.28.0 // indirect
|
||||
google.golang.org/protobuf v1.36.8 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
golang.org/x/sync v0.17.0 // indirect
|
||||
golang.org/x/text v0.29.0 // indirect
|
||||
golang.org/x/tools v0.36.0 // indirect
|
||||
google.golang.org/protobuf v1.36.9 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
|
|
|||
104
go.sum
104
go.sum
|
|
@ -1,7 +1,5 @@
|
|||
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
|
||||
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d h1:G0m3OIz70MZUWq3EgK3CesDbo8upS2Vm9/P3FtgI+Jk=
|
||||
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
|
||||
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
|
||||
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
|
||||
github.com/beevik/ntp v1.4.3 h1:PlbTvE5NNy4QHmA4Mg57n7mcFTmr1W1j3gcK7L1lqho=
|
||||
|
|
@ -10,20 +8,18 @@ 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/bool64/dev v0.2.39 h1:kP8DnMGlWXhGYJEZE/J0l/gVBdbuhoPGL+MJG4QbofE=
|
||||
github.com/bool64/dev v0.2.39/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg=
|
||||
github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0=
|
||||
github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
|
||||
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
|
||||
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
|
||||
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
|
||||
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chemhack/go-nbd v0.0.0-20241006125820-59e45f5b1e7b h1:dSbDgy72Y1sjLPWLv7vs0fMFuhMBMViiT9PJZiZWZNs=
|
||||
github.com/chemhack/go-nbd v0.0.0-20241006125820-59e45f5b1e7b/go.mod h1:SehHnbi2e8NiSAKby42Itm8SIoS7b+wAprsfPH3qgYk=
|
||||
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
|
||||
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE=
|
||||
github.com/coder/websocket v1.8.13/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
|
||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||
github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
|
||||
github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
|
||||
github.com/coreos/go-oidc/v3 v3.15.0 h1:R6Oz8Z4bqWR7VFQ+sPSvZPQv4x8M+sJkDO5ojgwlyAg=
|
||||
github.com/coreos/go-oidc/v3 v3.15.0/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
|
|
@ -44,50 +40,42 @@ github.com/gin-contrib/logger v1.2.6 h1:EPolruKUTzNXMVBD9LuAFQmRjTs7AH7yKGuXgYqr
|
|||
github.com/gin-contrib/logger v1.2.6/go.mod h1:7niPrd7F0Nscw/zvgz8RiGJxSdbKM2yfQNy8xCHcm64=
|
||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
|
||||
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
github.com/go-co-op/gocron/v2 v2.16.5 h1:j228Jxk7bb9CF8LKR3gS+bK3rcjRUINjlVI+ZMp26Ss=
|
||||
github.com/go-co-op/gocron/v2 v2.16.5/go.mod h1:zAfC/GFQ668qHxOVl/D68Jh5Ce7sDqX6TJnSQyRkRBc=
|
||||
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
|
||||
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
|
||||
github.com/go-co-op/gocron/v2 v2.16.6 h1:zI2Ya9sqvuLcgqJgV79LwoJXM8h20Z/drtB7ATbpRWo=
|
||||
github.com/go-co-op/gocron/v2 v2.16.6/go.mod h1:zAfC/GFQ668qHxOVl/D68Jh5Ce7sDqX6TJnSQyRkRBc=
|
||||
github.com/go-jose/go-jose/v4 v4.1.0 h1:cYSYxd3pw5zd2FSXk2vGdn9igQU2PS8MuxrCOCl0FdY=
|
||||
github.com/go-jose/go-jose/v4 v4.1.0/go.mod h1:GG/vqmYm3Von2nYiB2vGTXzdoNKE5tix5tuc6iAd+sw=
|
||||
github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI=
|
||||
github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
|
||||
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/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
|
||||
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
|
||||
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||
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-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
||||
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrRoT6yV5+wkrOpcszoIsO4+4ds248=
|
||||
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk=
|
||||
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/gwatts/rootcerts v0.0.0-20250901182336-dc5ae18bd79f h1:08t2PbrkDgW2+mwCQ3jhKUBrCM9Bc9SeH5j2Dst3B+0=
|
||||
github.com/gwatts/rootcerts v0.0.0-20250901182336-dc5ae18bd79f/go.mod h1:5Kt9XkWvkGi2OHOq0QsGxebHmhCcqJ8KCbNg/a6+n+g=
|
||||
github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=
|
||||
github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=
|
||||
github.com/jpillora/overseer v1.1.6 h1:3ygYfNcR3FfOr22miu3vR1iQcXKMHbmULBh98rbkIyo=
|
||||
github.com/jpillora/overseer v1.1.6/go.mod h1:aPXQtxuVb9PVWRWTXpo+LdnC/YXQ0IBLNXqKMJmgk88=
|
||||
github.com/jpillora/s3 v1.1.4 h1:YCCKDWzb/Ye9EBNd83ATRF/8wPEy0xd43Rezb6u6fzc=
|
||||
github.com/jpillora/s3 v1.1.4/go.mod h1:yedE603V+crlFi1Kl/5vZJaBu9pUzE9wvKegU/lF2zs=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
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.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
|
|
@ -152,16 +140,20 @@ github.com/pion/webrtc/v4 v4.1.4/go.mod h1:Oab9npu1iZtQRMic3K3toYq5zFPvToe/QBw7d
|
|||
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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc=
|
||||
github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE=
|
||||
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.66.0 h1:K/rJPHrG3+AoQs50r2+0t7zMnMzek2Vbv31OFVsMeVY=
|
||||
github.com/prometheus/common v0.66.0/go.mod h1:Ux6NtV1B4LatamKE63tJBntoxD++xmtI/lK0VtEplN4=
|
||||
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
|
||||
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
|
||||
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
|
||||
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
|
||||
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/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
||||
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
|
||||
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
|
|
@ -170,15 +162,12 @@ github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
|
|||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||
github.com/smartystreets/assertions v1.0.1/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM=
|
||||
github.com/smartystreets/gunit v1.1.3/go.mod h1:EH5qMBab2UclzXUcpR8b93eHsIlp9u+pDQIRp5DZNzQ=
|
||||
github.com/sourcegraph/tf-dag v0.2.2-0.20250131204052-3e8ff1477b4f h1:VgoRCP1efSCEZIcF2THLQ46+pIBzzgNiaUBe9wEDwYU=
|
||||
github.com/sourcegraph/tf-dag v0.2.2-0.20250131204052-3e8ff1477b4f/go.mod h1:pzro7BGorij2WgrjEammtrkbo3+xldxo+KaGLGUiD+Q=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
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.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.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
|
|
@ -200,33 +189,40 @@ go.bug.st/serial v1.6.4 h1:7FmqNPgVp3pu2Jz5PoPtbZ9jJO5gnEnZIvnI1lzve8A=
|
|||
go.bug.st/serial v1.6.4/go.mod h1:nofMJxTeNVny/m6+KaafC6vJGj3miwQZ6vW4BZUGJPI=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
golang.org/x/arch v0.18.0 h1:WN9poc33zL4AzGxqf8VtpKUnGvMi8O9lhNyBMF/85qc=
|
||||
golang.org/x/arch v0.18.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
||||
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
||||
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
|
||||
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
|
||||
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
|
||||
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
|
||||
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
|
||||
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
||||
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
|
||||
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
|
||||
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
|
||||
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
|
||||
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
|
||||
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
|
||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
|
||||
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
|
||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
|
||||
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
|
||||
golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
|
||||
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
||||
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
||||
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
|
||||
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
|
||||
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
|
||||
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
|
|
|
|||
|
|
@ -1038,6 +1038,15 @@ func setKeyboardMacros(params KeyboardMacrosParams) (any, error) {
|
|||
step.Delay = int(delay)
|
||||
}
|
||||
|
||||
// Optional text field for advanced steps
|
||||
if txt, ok := stepMap["text"].(string); ok {
|
||||
step.Text = txt
|
||||
}
|
||||
|
||||
if wv, ok := stepMap["wait"].(bool); ok {
|
||||
step.Wait = wv
|
||||
}
|
||||
|
||||
steps = append(steps, step)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "kvm-ui",
|
||||
"private": true,
|
||||
"version": "2025.09.26.01300",
|
||||
"version": "2025.10.01.1900",
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": "^22.15.0"
|
||||
|
|
@ -42,7 +42,7 @@
|
|||
"react-hot-toast": "^2.6.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-router": "^7.9.3",
|
||||
"react-simple-keyboard": "^3.8.122",
|
||||
"react-simple-keyboard": "^3.8.125",
|
||||
"react-use-websocket": "^4.13.0",
|
||||
"react-xtermjs": "^1.0.10",
|
||||
"recharts": "^3.2.1",
|
||||
|
|
@ -56,15 +56,15 @@
|
|||
"@eslint/eslintrc": "^3.3.1",
|
||||
"@eslint/js": "^9.36.0",
|
||||
"@tailwindcss/forms": "^0.5.10",
|
||||
"@tailwindcss/postcss": "^4.1.13",
|
||||
"@tailwindcss/postcss": "^4.1.14",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@tailwindcss/vite": "^4.1.13",
|
||||
"@types/react": "^19.1.14",
|
||||
"@types/react-dom": "^19.1.9",
|
||||
"@tailwindcss/vite": "^4.1.14",
|
||||
"@types/react": "^19.1.17",
|
||||
"@types/react-dom": "^19.1.10",
|
||||
"@types/semver": "^7.7.1",
|
||||
"@types/validator": "^13.15.3",
|
||||
"@typescript-eslint/eslint-plugin": "^8.44.1",
|
||||
"@typescript-eslint/parser": "^8.44.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.45.0",
|
||||
"@typescript-eslint/parser": "^8.45.0",
|
||||
"@vitejs/plugin-react-swc": "^4.1.0",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"eslint": "^9.36.0",
|
||||
|
|
@ -77,8 +77,8 @@
|
|||
"postcss": "^8.5.6",
|
||||
"prettier": "^3.6.2",
|
||||
"prettier-plugin-tailwindcss": "^0.6.14",
|
||||
"tailwindcss": "^4.1.13",
|
||||
"typescript": "^5.9.2",
|
||||
"tailwindcss": "^4.1.14",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.1.7",
|
||||
"vite-tsconfig-paths": "^5.1.4"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ export function MacroForm({
|
|||
newErrors.steps = { 0: { keys: "At least one step is required" } };
|
||||
} else {
|
||||
const hasKeyOrModifier = macro.steps.some(
|
||||
step => (step.keys?.length || 0) > 0 || (step.modifiers?.length || 0) > 0,
|
||||
step => (step.text && step.text.length > 0) || (step.keys?.length || 0) > 0 || (step.modifiers?.length || 0) > 0,
|
||||
);
|
||||
|
||||
if (!hasKeyOrModifier) {
|
||||
|
|
@ -163,6 +163,40 @@ export function MacroForm({
|
|||
setMacro({ ...macro, steps: newSteps });
|
||||
};
|
||||
|
||||
const handleStepTypeChange = (stepIndex: number, type: "keys" | "text" | "wait") => {
|
||||
const newSteps = [...(macro.steps || [])];
|
||||
const prev = newSteps[stepIndex] || { keys: [], modifiers: [], delay: DEFAULT_DELAY };
|
||||
if (type === "text") {
|
||||
newSteps[stepIndex] = { keys: [], modifiers: [], delay: prev.delay, text: prev.text || "" } as any;
|
||||
} else if (type === "wait") {
|
||||
newSteps[stepIndex] = { keys: [], modifiers: [], delay: prev.delay, wait: true } as any;
|
||||
} else {
|
||||
// switch back to keys; drop text
|
||||
const { text, wait, ...rest } = prev as any;
|
||||
newSteps[stepIndex] = { ...rest } as any;
|
||||
}
|
||||
setMacro({ ...macro, steps: newSteps });
|
||||
};
|
||||
|
||||
const handleTextChange = (stepIndex: number, text: string) => {
|
||||
const newSteps = [...(macro.steps || [])];
|
||||
// Ensure this step is of text type
|
||||
newSteps[stepIndex] = { ...(newSteps[stepIndex] || { keys: [], modifiers: [], delay: DEFAULT_DELAY }), text } as any;
|
||||
setMacro({ ...macro, steps: newSteps });
|
||||
};
|
||||
|
||||
const insertStepAfter = (index: number) => {
|
||||
if (isMaxStepsReached) {
|
||||
showTemporaryError(
|
||||
`You can only add a maximum of ${MAX_STEPS_PER_MACRO} steps per macro.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
const newSteps = [...(macro.steps || [])];
|
||||
newSteps.splice(index + 1, 0, { keys: [], modifiers: [], delay: DEFAULT_DELAY });
|
||||
setMacro(prev => ({ ...prev, steps: newSteps }));
|
||||
};
|
||||
|
||||
const handleStepMove = (stepIndex: number, direction: "up" | "down") => {
|
||||
const newSteps = [...(macro.steps || [])];
|
||||
const newIndex = direction === "up" ? stepIndex - 1 : stepIndex + 1;
|
||||
|
|
@ -213,8 +247,8 @@ export function MacroForm({
|
|||
<Fieldset>
|
||||
<div className="mt-2 space-y-4">
|
||||
{(macro.steps || []).map((step, stepIndex) => (
|
||||
<div key={stepIndex} className="space-y-3">
|
||||
<MacroStepCard
|
||||
key={stepIndex}
|
||||
step={step}
|
||||
stepIndex={stepIndex}
|
||||
onDelete={
|
||||
|
|
@ -237,7 +271,22 @@ export function MacroForm({
|
|||
onDelayChange={delay => handleDelayChange(stepIndex, delay)}
|
||||
isLastStep={stepIndex === (macro.steps?.length || 0) - 1}
|
||||
keyboard={selectedKeyboard}
|
||||
onStepTypeChange={type => handleStepTypeChange(stepIndex, type)}
|
||||
onTextChange={text => handleTextChange(stepIndex, text)}
|
||||
/>
|
||||
{stepIndex < (macro.steps?.length || 0) - 1 && (
|
||||
<div className="flex justify-center">
|
||||
<Button
|
||||
size="XS"
|
||||
theme="light"
|
||||
LeadingIcon={LuPlus}
|
||||
text="Insert step here"
|
||||
onClick={() => insertStepAfter(stepIndex)}
|
||||
disabled={isMaxStepsReached}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Fieldset>
|
||||
|
|
|
|||
|
|
@ -38,16 +38,22 @@ const basePresetDelays = [
|
|||
];
|
||||
|
||||
const PRESET_DELAYS = basePresetDelays.map(delay => {
|
||||
if (parseInt(delay.value, 10) === DEFAULT_DELAY) {
|
||||
return { ...delay, label: "Default" };
|
||||
}
|
||||
if (parseInt(delay.value, 10) === DEFAULT_DELAY) return { ...delay, label: "Default" };
|
||||
return delay;
|
||||
});
|
||||
|
||||
const TEXT_EXTRA_DELAYS = [
|
||||
{ value: "10", label: "10ms" },
|
||||
{ value: "20", label: "20ms" },
|
||||
{ value: "30", label: "30ms" },
|
||||
];
|
||||
|
||||
interface MacroStep {
|
||||
keys: string[];
|
||||
modifiers: string[];
|
||||
delay: number;
|
||||
text?: string;
|
||||
wait?: boolean;
|
||||
}
|
||||
|
||||
interface MacroStepCardProps {
|
||||
|
|
@ -62,7 +68,9 @@ interface MacroStepCardProps {
|
|||
onModifierChange: (modifiers: string[]) => void;
|
||||
onDelayChange: (delay: number) => void;
|
||||
isLastStep: boolean;
|
||||
keyboard: KeyboardLayout
|
||||
keyboard: KeyboardLayout;
|
||||
onStepTypeChange: (type: "keys" | "text" | "wait") => void;
|
||||
onTextChange: (text: string) => void;
|
||||
}
|
||||
|
||||
const ensureArray = <T,>(arr: T[] | null | undefined): T[] => {
|
||||
|
|
@ -81,7 +89,9 @@ export function MacroStepCard({
|
|||
onModifierChange,
|
||||
onDelayChange,
|
||||
isLastStep,
|
||||
keyboard
|
||||
keyboard,
|
||||
onStepTypeChange,
|
||||
onTextChange,
|
||||
}: MacroStepCardProps) {
|
||||
const { keyDisplayMap } = keyboard;
|
||||
|
||||
|
|
@ -106,6 +116,8 @@ export function MacroStepCard({
|
|||
}
|
||||
}, [keyOptions, keyQuery, step.keys]);
|
||||
|
||||
const stepType: "keys" | "text" | "wait" = step.wait ? "wait" : (step.text !== undefined ? "text" : "keys");
|
||||
|
||||
return (
|
||||
<Card className="p-4">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
|
|
@ -146,6 +158,46 @@ export function MacroStepCard({
|
|||
</div>
|
||||
|
||||
<div className="space-y-4 mt-2">
|
||||
<div className="w-full flex flex-col gap-2">
|
||||
<FieldLabel label="Step Type" />
|
||||
<div className="inline-flex gap-2">
|
||||
<Button
|
||||
size="XS"
|
||||
theme={stepType === "keys" ? "primary" : "light"}
|
||||
text="Keys/Modifiers"
|
||||
onClick={() => onStepTypeChange("keys")}
|
||||
/>
|
||||
<Button
|
||||
size="XS"
|
||||
theme={stepType === "text" ? "primary" : "light"}
|
||||
text="Text"
|
||||
onClick={() => onStepTypeChange("text")}
|
||||
/>
|
||||
<Button
|
||||
size="XS"
|
||||
theme={stepType === "wait" ? "primary" : "light"}
|
||||
text="Wait"
|
||||
onClick={() => onStepTypeChange("wait")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{stepType === "text" ? (
|
||||
<div className="w-full flex flex-col gap-1">
|
||||
<FieldLabel label="Text to type" description="Will be typed with this step's delay per character" />
|
||||
<input
|
||||
type="text"
|
||||
className="w-full rounded-md border border-slate-200 px-2 py-1 text-sm dark:border-slate-700 dark:bg-slate-800"
|
||||
value={step.text || ""}
|
||||
onChange={e => onTextChange(e.target.value)}
|
||||
placeholder="Enter text..."
|
||||
/>
|
||||
</div>
|
||||
) : stepType === "wait" ? (
|
||||
<div className="w-full flex flex-col gap-1">
|
||||
<FieldLabel label="Wait" description="Pause execution for the specified duration." />
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400">This step waits for the configured duration, no keys are sent.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-full flex flex-col gap-2">
|
||||
<FieldLabel label="Modifiers" />
|
||||
<div className="inline-flex flex-wrap gap-3">
|
||||
|
|
@ -176,7 +228,8 @@ export function MacroStepCard({
|
|||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
)}
|
||||
{stepType === "keys" && (
|
||||
<div className="w-full flex flex-col gap-1">
|
||||
<div className="flex items-center gap-1">
|
||||
<FieldLabel label="Keys" description={`Maximum ${MAX_KEYS_PER_STEP} keys per step.`} />
|
||||
|
|
@ -223,10 +276,10 @@ export function MacroStepCard({
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
)}
|
||||
<div className="w-full flex flex-col gap-1">
|
||||
<div className="flex items-center gap-1">
|
||||
<FieldLabel label="Step Duration" description="Time to wait before executing the next step." />
|
||||
<FieldLabel label="Step Duration" description={stepType === "text" ? "Delay per character when typing text" : stepType === "wait" ? "How long to pause before the next step" : "Time to wait before executing the next step."} />
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<SelectMenuBasic
|
||||
|
|
@ -234,10 +287,11 @@ export function MacroStepCard({
|
|||
fullWidth
|
||||
value={step.delay.toString()}
|
||||
onChange={(e) => onDelayChange(parseInt(e.target.value, 10))}
|
||||
options={PRESET_DELAYS}
|
||||
options={stepType === 'text' ? [...TEXT_EXTRA_DELAYS, ...PRESET_DELAYS] : PRESET_DELAYS}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -763,6 +763,8 @@ export interface KeySequenceStep {
|
|||
keys: string[];
|
||||
modifiers: string[];
|
||||
delay: number;
|
||||
text?: string; // optional: when set, type this text with per-character delay
|
||||
wait?: boolean; // optional: when true, this is a pure wait step (pause for delay ms)
|
||||
}
|
||||
|
||||
export interface KeySequence {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import {
|
|||
import { useHidRpc } from "@/hooks/useHidRpc";
|
||||
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import { hidKeyToModifierMask, keys, modifiers } from "@/keyboardMappings";
|
||||
import useKeyboardLayout from "@/hooks/useKeyboardLayout";
|
||||
|
||||
const MACRO_RESET_KEYBOARD_STATE = {
|
||||
keys: new Array(hidKeyBufferSize).fill(0),
|
||||
|
|
@ -27,6 +28,8 @@ export interface MacroStep {
|
|||
keys: string[] | null;
|
||||
modifiers: string[] | null;
|
||||
delay: number;
|
||||
text?: string | undefined;
|
||||
wait?: boolean | undefined;
|
||||
}
|
||||
|
||||
export type MacroSteps = MacroStep[];
|
||||
|
|
@ -34,6 +37,7 @@ export type MacroSteps = MacroStep[];
|
|||
const sleep = (ms: number): Promise<void> => new Promise(resolve => setTimeout(resolve, ms));
|
||||
|
||||
export default function useKeyboard() {
|
||||
const { selectedKeyboard } = useKeyboardLayout();
|
||||
const { send } = useJsonRpc();
|
||||
const { rpcDataChannel } = useRTCStore();
|
||||
const { keysDownState, setKeysDownState, setKeyboardLedState, setPasteModeEnabled } =
|
||||
|
|
@ -284,9 +288,38 @@ export default function useKeyboard() {
|
|||
// After the delay, the keys and modifiers are released and the next step is executed.
|
||||
// If a step has no keys or modifiers, it is treated as a delay-only step.
|
||||
// A small pause is added between steps to ensure that the device can process the events.
|
||||
const expandTextSteps = useCallback((steps: MacroSteps): MacroSteps => {
|
||||
const expanded: MacroSteps = [];
|
||||
for (const step of steps) {
|
||||
if (step.text && step.text.length > 0 && selectedKeyboard) {
|
||||
for (const char of step.text) {
|
||||
const keyprops = selectedKeyboard.chars[char];
|
||||
if (!keyprops) continue;
|
||||
const { key, shift, altRight, deadKey, accentKey } = keyprops;
|
||||
if (!key) continue;
|
||||
if (accentKey) {
|
||||
const accentModifiers: string[] = [];
|
||||
if (accentKey.shift) accentModifiers.push("ShiftLeft");
|
||||
if (accentKey.altRight) accentModifiers.push("AltRight");
|
||||
expanded.push({ keys: [String(accentKey.key)], modifiers: accentModifiers, delay: step.delay });
|
||||
}
|
||||
const mods: string[] = [];
|
||||
if (shift) mods.push("ShiftLeft");
|
||||
if (altRight) mods.push("AltRight");
|
||||
expanded.push({ keys: [String(key)], modifiers: mods, delay: step.delay });
|
||||
if (deadKey) expanded.push({ keys: ["Space"], modifiers: null, delay: step.delay });
|
||||
}
|
||||
} else {
|
||||
expanded.push(step);
|
||||
}
|
||||
}
|
||||
return expanded;
|
||||
}, [selectedKeyboard]);
|
||||
|
||||
const executeMacroRemote = useCallback(async (
|
||||
steps: MacroSteps,
|
||||
stepsIn: MacroSteps,
|
||||
) => {
|
||||
const steps = expandTextSteps(stepsIn);
|
||||
const macro: KeyboardMacroStep[] = [];
|
||||
|
||||
for (const [_, step] of steps.entries()) {
|
||||
|
|
@ -297,16 +330,22 @@ export default function useKeyboard() {
|
|||
|
||||
.reduce((acc, val) => acc + val, 0);
|
||||
|
||||
// If the step has keys and/or modifiers, press them and hold for the delay
|
||||
if (keyValues.length > 0 || modifierMask > 0) {
|
||||
if (step.wait) {
|
||||
// pure wait: send a no-op clear state with desired delay
|
||||
macro.push({ ...MACRO_RESET_KEYBOARD_STATE, delay: step.delay || 100 });
|
||||
} else if (keyValues.length > 0 || modifierMask > 0) {
|
||||
macro.push({ keys: keyValues, modifier: modifierMask, delay: 20 });
|
||||
macro.push({ ...MACRO_RESET_KEYBOARD_STATE, delay: step.delay || 100 });
|
||||
} else {
|
||||
// empty step (pause only)
|
||||
macro.push({ ...MACRO_RESET_KEYBOARD_STATE, delay: step.delay || 100 });
|
||||
}
|
||||
}
|
||||
|
||||
sendKeyboardMacroEventHidRpc(macro);
|
||||
}, [sendKeyboardMacroEventHidRpc]);
|
||||
const executeMacroClientSide = useCallback(async (steps: MacroSteps) => {
|
||||
}, [sendKeyboardMacroEventHidRpc, expandTextSteps]);
|
||||
const executeMacroClientSide = useCallback(async (stepsIn: MacroSteps) => {
|
||||
const steps = expandTextSteps(stepsIn);
|
||||
const promises: (() => Promise<void>)[] = [];
|
||||
|
||||
const ac = new AbortController();
|
||||
|
|
@ -318,11 +357,14 @@ export default function useKeyboard() {
|
|||
.map(mod => modifiers[mod])
|
||||
.reduce((acc, val) => acc + val, 0);
|
||||
|
||||
// If the step has keys and/or modifiers, press them and hold for the delay
|
||||
if (keyValues.length > 0 || modifierMask > 0) {
|
||||
if (step.wait) {
|
||||
promises.push(() => sleep(step.delay || 100));
|
||||
} else if (keyValues.length > 0 || modifierMask > 0) {
|
||||
promises.push(() => sendKeystrokeLegacy(keyValues, modifierMask, ac));
|
||||
promises.push(() => resetKeyboardState());
|
||||
promises.push(() => sleep(step.delay || 100));
|
||||
} else {
|
||||
promises.push(() => sleep(step.delay || 100));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -354,7 +396,7 @@ export default function useKeyboard() {
|
|||
reject(error);
|
||||
});
|
||||
});
|
||||
}, [sendKeystrokeLegacy, resetKeyboardState, setAbortController]);
|
||||
}, [sendKeystrokeLegacy, resetKeyboardState, setAbortController, expandTextSteps]);
|
||||
const executeMacro = useCallback(async (steps: MacroSteps) => {
|
||||
if (rpcHidReady) {
|
||||
return executeMacroRemote(steps);
|
||||
|
|
|
|||
|
|
@ -153,13 +153,13 @@ body {
|
|||
|
||||
@property --grid-color-start {
|
||||
syntax: "<color>";
|
||||
initial-value: var(--color-blue-50/10);
|
||||
initial-value: oklch(97% 0.014 254.604 / 10); /* var(--color-blue-50/10) */
|
||||
inherits: false;
|
||||
}
|
||||
|
||||
@property --grid-color-end {
|
||||
syntax: "<color>";
|
||||
initial-value: var(--color-blue-50/100);
|
||||
initial-value: oklch(97% 0.014 254.604 / 100); /* var(--color-blue-50/100) */
|
||||
inherits: false;
|
||||
}
|
||||
|
||||
|
|
@ -175,8 +175,8 @@ body {
|
|||
}
|
||||
|
||||
.group:hover .grid-card {
|
||||
--grid-color-start: var(--color-blue-100/50);
|
||||
--grid-color-end: var(--color-blue-50/50);
|
||||
--grid-color-start: oklch(from var(--color-blue-100) l c h / 50);
|
||||
--grid-color-end: oklch(from var(--color-blue-50) l c h / 50);
|
||||
}
|
||||
|
||||
video::-webkit-media-controls {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useEffect, Fragment, useMemo, useState, useCallback } from "react";
|
||||
import { useEffect, Fragment, useMemo, useState, useCallback, useRef } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
import {
|
||||
LuPenLine,
|
||||
|
|
@ -9,6 +9,7 @@ import {
|
|||
LuArrowDown,
|
||||
LuTrash2,
|
||||
LuCommand,
|
||||
LuDownload,
|
||||
} from "react-icons/lu";
|
||||
|
||||
import { KeySequence, useMacrosStore, generateMacroId } from "@/hooks/stores";
|
||||
|
|
@ -22,13 +23,32 @@ import { ConfirmDialog } from "@/components/ConfirmDialog";
|
|||
import LoadingSpinner from "@/components/LoadingSpinner";
|
||||
import useKeyboardLayout from "@/hooks/useKeyboardLayout";
|
||||
|
||||
const normalizeSortOrders = (macros: KeySequence[]): KeySequence[] => {
|
||||
return macros.map((macro, index) => ({
|
||||
...macro,
|
||||
sortOrder: index + 1,
|
||||
}));
|
||||
const normalizeSortOrders = (macros: KeySequence[]): KeySequence[] => macros.map((m, i) => ({ ...m, sortOrder: i + 1 }));
|
||||
|
||||
const pad2 = (n: number) => String(n).padStart(2, "0");
|
||||
|
||||
const buildMacroDownloadFilename = (macro: KeySequence) => {
|
||||
const safeName = (macro.name || macro.id).replace(/[^a-z0-9-_]+/gi, "-").toLowerCase();
|
||||
const now = new Date();
|
||||
const ts = `${now.getFullYear()}${pad2(now.getMonth() + 1)}${pad2(now.getDate())}-${pad2(now.getHours())}${pad2(now.getMinutes())}${pad2(now.getSeconds())}`;
|
||||
return `jetkvm-macro-${safeName}-${ts}.json`;
|
||||
};
|
||||
|
||||
const sanitizeImportedStep = (raw: any) => ({
|
||||
keys: Array.isArray(raw?.keys) ? raw.keys.filter((k: any) => typeof k === "string") : [],
|
||||
modifiers: Array.isArray(raw?.modifiers) ? raw.modifiers.filter((m: any) => typeof m === "string") : [],
|
||||
delay: typeof raw?.delay === "number" ? raw.delay : DEFAULT_DELAY,
|
||||
text: typeof raw?.text === "string" ? raw.text : undefined,
|
||||
wait: typeof raw?.wait === "boolean" ? raw.wait : false,
|
||||
});
|
||||
|
||||
const sanitizeImportedMacro = (raw: any, sortOrder: number): KeySequence => ({
|
||||
id: generateMacroId(),
|
||||
name: (typeof raw?.name === "string" && raw.name.trim() ? raw.name : "Imported Macro").slice(0, 50),
|
||||
steps: Array.isArray(raw?.steps) ? raw.steps.map(sanitizeImportedStep) : [],
|
||||
sortOrder,
|
||||
});
|
||||
|
||||
export default function SettingsMacrosRoute() {
|
||||
const { macros, loading, initialized, loadMacros, saveMacros } = useMacrosStore();
|
||||
const navigate = useNavigate();
|
||||
|
|
@ -36,6 +56,7 @@ export default function SettingsMacrosRoute() {
|
|||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||
const [macroToDelete, setMacroToDelete] = useState<KeySequence | null>(null);
|
||||
const { selectedKeyboard } = useKeyboardLayout();
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const isMaxMacrosReached = useMemo(
|
||||
() => macros.length >= MAX_TOTAL_MACROS,
|
||||
|
|
@ -48,74 +69,52 @@ export default function SettingsMacrosRoute() {
|
|||
}
|
||||
}, [initialized, loadMacros]);
|
||||
|
||||
const handleDuplicateMacro = useCallback(
|
||||
async (macro: KeySequence) => {
|
||||
const handleDuplicateMacro = useCallback(async (macro: KeySequence) => {
|
||||
if (!macro?.id || !macro?.name) {
|
||||
notifications.error("Invalid macro data");
|
||||
return;
|
||||
}
|
||||
|
||||
if (isMaxMacrosReached) {
|
||||
notifications.error(`Maximum of ${MAX_TOTAL_MACROS} macros allowed`);
|
||||
return;
|
||||
}
|
||||
|
||||
setActionLoadingId(macro.id);
|
||||
|
||||
const newMacroCopy: KeySequence = {
|
||||
...JSON.parse(JSON.stringify(macro)),
|
||||
id: generateMacroId(),
|
||||
name: `${macro.name} ${COPY_SUFFIX}`,
|
||||
sortOrder: macros.length + 1,
|
||||
};
|
||||
|
||||
try {
|
||||
await saveMacros(normalizeSortOrders([...macros, newMacroCopy]));
|
||||
notifications.success(`Macro "${newMacroCopy.name}" duplicated successfully`);
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
notifications.error(`Failed to duplicate macro: ${error.message}`);
|
||||
} else {
|
||||
notifications.error("Failed to duplicate macro");
|
||||
}
|
||||
} catch (e: any) {
|
||||
notifications.error(`Failed to duplicate macro: ${e?.message || 'error'}`);
|
||||
} finally {
|
||||
setActionLoadingId(null);
|
||||
}
|
||||
},
|
||||
[isMaxMacrosReached, macros, saveMacros, setActionLoadingId],
|
||||
);
|
||||
}, [macros, saveMacros, isMaxMacrosReached]);
|
||||
|
||||
const handleMoveMacro = useCallback(
|
||||
async (index: number, direction: "up" | "down", macroId: string) => {
|
||||
const handleMoveMacro = useCallback(async (index: number, direction: "up" | "down", macroId: string) => {
|
||||
if (!Array.isArray(macros) || macros.length === 0) {
|
||||
notifications.error("No macros available");
|
||||
return;
|
||||
}
|
||||
|
||||
const newIndex = direction === "up" ? index - 1 : index + 1;
|
||||
if (newIndex < 0 || newIndex >= macros.length) return;
|
||||
|
||||
setActionLoadingId(macroId);
|
||||
|
||||
try {
|
||||
const newMacros = [...macros];
|
||||
[newMacros[index], newMacros[newIndex]] = [newMacros[newIndex], newMacros[index]];
|
||||
const updatedMacros = normalizeSortOrders(newMacros);
|
||||
|
||||
await saveMacros(updatedMacros);
|
||||
notifications.success("Macro order updated successfully");
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
notifications.error(`Failed to reorder macros: ${error.message}`);
|
||||
} else {
|
||||
notifications.error("Failed to reorder macros");
|
||||
}
|
||||
} catch (e: any) {
|
||||
notifications.error(`Failed to reorder macros: ${e?.message || 'error'}`);
|
||||
} finally {
|
||||
setActionLoadingId(null);
|
||||
}
|
||||
},
|
||||
[macros, saveMacros, setActionLoadingId],
|
||||
);
|
||||
}, [macros, saveMacros]);
|
||||
|
||||
const handleDeleteMacro = useCallback(async () => {
|
||||
if (!macroToDelete?.id) return;
|
||||
|
|
@ -140,6 +139,17 @@ export default function SettingsMacrosRoute() {
|
|||
}
|
||||
}, [macroToDelete, macros, saveMacros]);
|
||||
|
||||
const handleDownloadMacro = useCallback((macro: KeySequence) => {
|
||||
const data = JSON.stringify(macro, null, 2);
|
||||
const blob = new Blob([data], { type: "application/json" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = buildMacroDownloadFilename(macro);
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}, []);
|
||||
|
||||
const MacroList = useMemo(
|
||||
() => (
|
||||
<div className="space-y-2">
|
||||
|
|
@ -178,8 +188,11 @@ export default function SettingsMacrosRoute() {
|
|||
<span key={stepIndex} className="inline-flex items-center">
|
||||
<StepIcon className="mr-1 h-3 w-3 shrink-0 text-slate-400 dark:text-slate-500" />
|
||||
<span className="rounded-md border border-slate-200/50 bg-slate-50 px-2 py-0.5 dark:border-slate-700/50 dark:bg-slate-800">
|
||||
{(Array.isArray(step.modifiers) &&
|
||||
step.modifiers.length > 0) ||
|
||||
{step.text && step.text.length > 0 ? (
|
||||
<span className="font-medium text-emerald-700 dark:text-emerald-300">Text: "{step.text}"</span>
|
||||
) : step.wait ? (
|
||||
<span className="font-medium text-amber-600 dark:text-amber-300">Wait</span>
|
||||
) : (Array.isArray(step.modifiers) && step.modifiers.length > 0) ||
|
||||
(Array.isArray(step.keys) && step.keys.length > 0) ? (
|
||||
<>
|
||||
{Array.isArray(step.modifiers) &&
|
||||
|
|
@ -224,7 +237,7 @@ export default function SettingsMacrosRoute() {
|
|||
</>
|
||||
) : (
|
||||
<span className="font-medium text-slate-500 dark:text-slate-400">
|
||||
Delay only
|
||||
Pause only
|
||||
</span>
|
||||
)}
|
||||
{step.delay !== DEFAULT_DELAY && (
|
||||
|
|
@ -261,6 +274,7 @@ export default function SettingsMacrosRoute() {
|
|||
disabled={actionLoadingId === macro.id}
|
||||
aria-label={`Duplicate macro ${macro.name}`}
|
||||
/>
|
||||
<Button size="XS" theme="light" LeadingIcon={LuDownload} onClick={() => handleDownloadMacro(macro)} aria-label={`Download macro ${macro.name}`} disabled={actionLoadingId === macro.id} />
|
||||
<Button
|
||||
size="XS"
|
||||
theme="light"
|
||||
|
|
@ -290,19 +304,7 @@ export default function SettingsMacrosRoute() {
|
|||
/>
|
||||
</div>
|
||||
),
|
||||
[
|
||||
macros,
|
||||
showDeleteConfirm,
|
||||
macroToDelete?.name,
|
||||
macroToDelete?.id,
|
||||
actionLoadingId,
|
||||
handleDeleteMacro,
|
||||
handleMoveMacro,
|
||||
selectedKeyboard.modifierDisplayMap,
|
||||
selectedKeyboard.keyDisplayMap,
|
||||
handleDuplicateMacro,
|
||||
navigate
|
||||
],
|
||||
[macros, showDeleteConfirm, macroToDelete?.name, macroToDelete?.id, actionLoadingId, handleDeleteMacro, handleMoveMacro, selectedKeyboard.modifierDisplayMap, selectedKeyboard.keyDisplayMap, handleDuplicateMacro, navigate, handleDownloadMacro],
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
@ -312,7 +314,6 @@ export default function SettingsMacrosRoute() {
|
|||
title="Keyboard Macros"
|
||||
description={`Combine keystrokes into a single action for faster workflows.`}
|
||||
/>
|
||||
{macros.length > 0 && (
|
||||
<div className="flex items-center pl-2">
|
||||
<Button
|
||||
size="SM"
|
||||
|
|
@ -322,8 +323,48 @@ export default function SettingsMacrosRoute() {
|
|||
disabled={isMaxMacrosReached}
|
||||
aria-label="Add new macro"
|
||||
/>
|
||||
<div className="ml-2 flex items-center gap-2">
|
||||
<input ref={fileInputRef} type="file" accept="application/json" multiple className="hidden" onChange={async e => {
|
||||
const fl = e.target.files;
|
||||
if (!fl || fl.length === 0) return;
|
||||
let working = [...macros];
|
||||
const imported: string[] = [];
|
||||
let errors = 0;
|
||||
let skipped = 0;
|
||||
for (const f of Array.from(fl)) {
|
||||
if (working.length >= MAX_TOTAL_MACROS) { skipped++; continue; }
|
||||
try {
|
||||
const raw = await f.text();
|
||||
const parsed = JSON.parse(raw);
|
||||
const candidates = Array.isArray(parsed) ? parsed : [parsed];
|
||||
for (const c of candidates) {
|
||||
if (working.length >= MAX_TOTAL_MACROS) { skipped += (candidates.length - candidates.indexOf(c)); break; }
|
||||
if (!c || typeof c !== "object") { errors++; continue; }
|
||||
const sanitized = sanitizeImportedMacro(c, working.length + 1);
|
||||
working.push(sanitized);
|
||||
imported.push(sanitized.name);
|
||||
}
|
||||
} catch { errors++; }
|
||||
}
|
||||
try {
|
||||
if (imported.length) {
|
||||
await saveMacros(normalizeSortOrders(working));
|
||||
notifications.success(`Imported ${imported.length} macro${imported.length === 1 ? '' : 's'}`);
|
||||
}
|
||||
if (errors) notifications.error(`${errors} file${errors === 1 ? '' : 's'} failed`);
|
||||
if (skipped) notifications.error(`${skipped} macro${skipped === 1 ? '' : 's'} skipped (limit ${MAX_TOTAL_MACROS})`);
|
||||
} finally {
|
||||
if (fileInputRef.current) fileInputRef.current.value = '';
|
||||
}
|
||||
}} />
|
||||
<Button
|
||||
size="SM"
|
||||
theme="light"
|
||||
text="Import Macro"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
|
|
|
|||
Loading…
Reference in New Issue