Compare commits

...

4 Commits

Author SHA1 Message Date
Marc Brooks 98d99561e4
Merge 84e4b44df0 into 4a23f22a55 2025-07-11 11:24:43 +02:00
Marc Brooks 4a23f22a55
chore: upgrade ui packages (#571)
Move to current on all non-major upgrades
Fixes the tainted hardware WebGL video renderer if video settings are at default (1.0) values

## Runtime

|  Package | From  | To  |
|---|---|---|
| @headlessui/react | 2.2.3 | 2.2.4 |
| @vitejs/plugin-basic-ssl | 2.0.0 | 2.1.0 |
| cva | 1.0.0-beta.3 | 1.0.0-beta.4 |
| focus-trap-react | 11.0.3 | 11.0.4 |
| framer-motion | 12.11.5 | 12.23.0 |
| react-simple-keyboard | 3.8.72 | 3.8.89 |
| tailwind-merge | 3.3.0 | 3.3.1 |
| validator | 13.15.0 | 13.15.15 |

## Dev

|  Package | From  | To  |
|---|---|---|
| @eslint/compat | 1.2.9 | 1.3.1 |
| @eslint/js | 9.26.0 | 9.30.1 |
| @tailwindcss/postcss | 4.1.7 | 4.1.11 |
| @tailwindcss/vite | 4.1.8 | 4.1.10 |
| @types/react | 19.1.4 | 19.1.8  |
| @types/react-dom | 19.1.5 | 19.1.6 |
| @types/validator | 13.15.0 | 13.15.2 |
| @typescript-eslint/eslint-plugin | 8.32.1 | 8.34.0 |
| @typescript-eslint/parser | 8.32.1 | 8.35.1  |
| @vitejs/plugin-react-swc | 3.9.0 | 3.10.2 |
| eslint | 9.26.0 | 9.30.1 |
| globals | 16.1.0 | 16.3.0 |
| postcss  | 8.5.3 | 8.5.6 |
| prettier | 3.5.3 | 3.6.2 |
| prettier-plugin-tailwindcss | 0.6.11 | 0.6.13 |
| tailwindcss | 4.1.7 | 4.1.11 |
2025-07-11 08:06:17 +02:00
Marc Brooks 11a095c0f6
feat(ntp): enhances time sync with DHCP NTP and custom servers (#625)
* Ensure the mDNS mode is set every time network state changes

Eliminates (mostly) duplicate code

* Add custom NTP and HTTP time sync servers

Since the ordering may have been previously defaulted and saved as "ntp,http", but that was being ignored and fallback-defaults were being used, in Ordering, `ntp` means use the fallback NTP servers, and `http` means use the fallback HTTP URLs. Thus `ntp_user_provided` and `http_user_provided` are the user specified static lists.

* Add support for using DHCP-provided NTP server
2025-07-11 08:04:19 +02:00
Marc Brooks 84e4b44df0
Treats all modifiers as "sticky" keys for use with just touch/mouse
Made the operation keys look more like a keyboard
Add persistent state management for the Shift/Ctrl/Alt/Meta
Add infoBar indicators for Shift/Ctr/Alt/Meta/AltGr
Remove redundant Break it's actually on keyboard as (Pause)
Add a style for the virtual keyboard "depressed" buttons.
Delete the no-longer-needed button variants
Added missing keycodes
Added modifier tracking for all the other keys
Ensures the InfoBar tracks for physical and virtual keyboard
Shows what buttons are depressed for sticky keys
Now treats all the Shift/Control/Alt/Meta/AltGr keys as if they were sticky keys so users can click the button and hit the next key,
2025-06-17 11:19:00 -05:00
16 changed files with 1271 additions and 849 deletions

View File

@ -43,9 +43,11 @@ type testNetworkConfig struct {
LLDPTxTLVs []string `json:"lldp_tx_tlvs,omitempty" one_of:"chassis,port,system,vlan" default:"chassis,port,system,vlan"` LLDPTxTLVs []string `json:"lldp_tx_tlvs,omitempty" one_of:"chassis,port,system,vlan" default:"chassis,port,system,vlan"`
MDNSMode null.String `json:"mdns_mode,omitempty" one_of:"disabled,auto,ipv4_only,ipv6_only" default:"auto"` MDNSMode null.String `json:"mdns_mode,omitempty" one_of:"disabled,auto,ipv4_only,ipv6_only" default:"auto"`
TimeSyncMode null.String `json:"time_sync_mode,omitempty" one_of:"ntp_only,ntp_and_http,http_only,custom" default:"ntp_and_http"` TimeSyncMode null.String `json:"time_sync_mode,omitempty" one_of:"ntp_only,ntp_and_http,http_only,custom" default:"ntp_and_http"`
TimeSyncOrdering []string `json:"time_sync_ordering,omitempty" one_of:"http,ntp,ntp_dhcp,ntp_user_provided,ntp_fallback" default:"ntp,http"` TimeSyncOrdering []string `json:"time_sync_ordering,omitempty" one_of:"http,ntp,ntp_dhcp,ntp_user_provided,http_user_provided" default:"ntp,http"`
TimeSyncDisableFallback null.Bool `json:"time_sync_disable_fallback,omitempty" default:"false"` TimeSyncDisableFallback null.Bool `json:"time_sync_disable_fallback,omitempty" default:"false"`
TimeSyncParallel null.Int `json:"time_sync_parallel,omitempty" default:"4"` TimeSyncParallel null.Int `json:"time_sync_parallel,omitempty" default:"4"`
TimeSyncNTPServers []string `json:"time_sync_ntp_servers,omitempty" validate_type:"ipv4_or_ipv6" required_if:"TimeSyncOrdering=ntp_user_provided"`
TimeSyncHTTPUrls []string `json:"time_sync_http_urls,omitempty" validate_type:"url" required_if:"TimeSyncOrdering=http_user_provided"`
} }
func TestValidateConfig(t *testing.T) { func TestValidateConfig(t *testing.T) {

View File

@ -45,9 +45,11 @@ type NetworkConfig struct {
LLDPTxTLVs []string `json:"lldp_tx_tlvs,omitempty" one_of:"chassis,port,system,vlan" default:"chassis,port,system,vlan"` LLDPTxTLVs []string `json:"lldp_tx_tlvs,omitempty" one_of:"chassis,port,system,vlan" default:"chassis,port,system,vlan"`
MDNSMode null.String `json:"mdns_mode,omitempty" one_of:"disabled,auto,ipv4_only,ipv6_only" default:"auto"` MDNSMode null.String `json:"mdns_mode,omitempty" one_of:"disabled,auto,ipv4_only,ipv6_only" default:"auto"`
TimeSyncMode null.String `json:"time_sync_mode,omitempty" one_of:"ntp_only,ntp_and_http,http_only,custom" default:"ntp_and_http"` TimeSyncMode null.String `json:"time_sync_mode,omitempty" one_of:"ntp_only,ntp_and_http,http_only,custom" default:"ntp_and_http"`
TimeSyncOrdering []string `json:"time_sync_ordering,omitempty" one_of:"http,ntp,ntp_dhcp,ntp_user_provided,ntp_fallback" default:"ntp,http"` TimeSyncOrdering []string `json:"time_sync_ordering,omitempty" one_of:"http,ntp,ntp_dhcp,ntp_user_provided,http_user_provided" default:"ntp,http"`
TimeSyncDisableFallback null.Bool `json:"time_sync_disable_fallback,omitempty" default:"false"` TimeSyncDisableFallback null.Bool `json:"time_sync_disable_fallback,omitempty" default:"false"`
TimeSyncParallel null.Int `json:"time_sync_parallel,omitempty" default:"4"` TimeSyncParallel null.Int `json:"time_sync_parallel,omitempty" default:"4"`
TimeSyncNTPServers []string `json:"time_sync_ntp_servers,omitempty" validate_type:"ipv4_or_ipv6" required_if:"TimeSyncOrdering=ntp_user_provided"`
TimeSyncHTTPUrls []string `json:"time_sync_http_urls,omitempty" validate_type:"url" required_if:"TimeSyncOrdering=http_user_provided"`
} }
func (c *NetworkConfig) GetMDNSMode() *mdns.MDNSListenOptions { func (c *NetworkConfig) GetMDNSMode() *mdns.MDNSListenOptions {

View File

@ -21,6 +21,7 @@ type NetworkInterfaceState struct {
ipv6Addr *net.IP ipv6Addr *net.IP
ipv6Addresses []IPv6Address ipv6Addresses []IPv6Address
ipv6LinkLocal *net.IP ipv6LinkLocal *net.IP
ntpAddresses []*net.IP
macAddr *net.HardwareAddr macAddr *net.HardwareAddr
l *zerolog.Logger l *zerolog.Logger
@ -76,6 +77,7 @@ func NewNetworkInterfaceState(opts *NetworkInterfaceOptions) (*NetworkInterfaceS
onInitialCheck: opts.OnInitialCheck, onInitialCheck: opts.OnInitialCheck,
cbConfigChange: opts.OnConfigChange, cbConfigChange: opts.OnConfigChange,
config: opts.NetworkConfig, config: opts.NetworkConfig,
ntpAddresses: make([]*net.IP, 0),
} }
// create the dhcp client // create the dhcp client
@ -89,7 +91,7 @@ func NewNetworkInterfaceState(opts *NetworkInterfaceOptions) (*NetworkInterfaceS
opts.Logger.Error().Err(err).Msg("failed to update network state") opts.Logger.Error().Err(err).Msg("failed to update network state")
return return
} }
_ = s.updateNtpServersFromLease(lease)
_ = s.setHostnameIfNotSame() _ = s.setHostnameIfNotSame()
opts.OnDhcpLeaseChange(lease) opts.OnDhcpLeaseChange(lease)
@ -135,6 +137,27 @@ func (s *NetworkInterfaceState) IPv6String() string {
return s.ipv6Addr.String() return s.ipv6Addr.String()
} }
func (s *NetworkInterfaceState) NtpAddresses() []*net.IP {
return s.ntpAddresses
}
func (s *NetworkInterfaceState) NtpAddressesString() []string {
ntpServers := []string{}
if s != nil {
s.l.Debug().Any("s", s).Msg("getting NTP address strings")
if len(s.ntpAddresses) > 0 {
for _, server := range s.ntpAddresses {
s.l.Debug().IPAddr("server", *server).Msg("converting NTP address")
ntpServers = append(ntpServers, server.String())
}
}
}
return ntpServers
}
func (s *NetworkInterfaceState) MAC() *net.HardwareAddr { func (s *NetworkInterfaceState) MAC() *net.HardwareAddr {
return s.macAddr return s.macAddr
} }
@ -318,6 +341,25 @@ func (s *NetworkInterfaceState) update() (DhcpTargetState, error) {
return dhcpTargetState, nil return dhcpTargetState, nil
} }
func (s *NetworkInterfaceState) updateNtpServersFromLease(lease *udhcpc.Lease) error {
if lease != nil && len(lease.NTPServers) > 0 {
s.l.Info().Msg("lease found, updating DHCP NTP addresses")
s.ntpAddresses = make([]*net.IP, 0, len(lease.NTPServers))
for _, ntpServer := range lease.NTPServers {
if ntpServer != nil {
s.l.Info().IPAddr("ntp_server", ntpServer).Msg("NTP server found in lease")
s.ntpAddresses = append(s.ntpAddresses, &ntpServer)
}
}
} else {
s.l.Info().Msg("no NTP servers found in lease")
s.ntpAddresses = make([]*net.IP, 0, len(s.config.TimeSyncNTPServers))
}
return nil
}
func (s *NetworkInterfaceState) CheckAndUpdateDhcp() error { func (s *NetworkInterfaceState) CheckAndUpdateDhcp() error {
dhcpTargetState, err := s.update() dhcpTargetState, err := s.update()
if err != nil { if err != nil {

View File

@ -19,9 +19,9 @@ var defaultHTTPUrls = []string{
// "http://www.msftconnecttest.com/connecttest.txt", // "http://www.msftconnecttest.com/connecttest.txt",
} }
func (t *TimeSync) queryAllHttpTime() (now *time.Time) { func (t *TimeSync) queryAllHttpTime(httpUrls []string) (now *time.Time) {
chunkSize := 4 chunkSize := int(t.networkConfig.TimeSyncParallel.ValueOr(4))
httpUrls := t.httpUrls t.l.Info().Strs("httpUrls", httpUrls).Int("chunkSize", chunkSize).Msg("querying HTTP URLs")
// shuffle the http urls to avoid always querying the same servers // shuffle the http urls to avoid always querying the same servers
rand.Shuffle(len(httpUrls), func(i, j int) { httpUrls[i], httpUrls[j] = httpUrls[j], httpUrls[i] }) rand.Shuffle(len(httpUrls), func(i, j int) { httpUrls[i], httpUrls[j] = httpUrls[j], httpUrls[i] })

View File

@ -73,6 +73,7 @@ var (
}, },
[]string{"url"}, []string{"url"},
) )
metricNtpServerInfo = promauto.NewGaugeVec( metricNtpServerInfo = promauto.NewGaugeVec(
prometheus.GaugeOpts{ prometheus.GaugeOpts{
Name: "jetkvm_timesync_ntp_server_info", Name: "jetkvm_timesync_ntp_server_info",

View File

@ -1,6 +1,7 @@
package timesync package timesync
import ( import (
"context"
"math/rand/v2" "math/rand/v2"
"strconv" "strconv"
"time" "time"
@ -21,9 +22,9 @@ var defaultNTPServers = []string{
"3.pool.ntp.org", "3.pool.ntp.org",
} }
func (t *TimeSync) queryNetworkTime() (now *time.Time, offset *time.Duration) { func (t *TimeSync) queryNetworkTime(ntpServers []string) (now *time.Time, offset *time.Duration) {
chunkSize := 4 chunkSize := int(t.networkConfig.TimeSyncParallel.ValueOr(4))
ntpServers := t.ntpServers t.l.Info().Strs("servers", ntpServers).Int("chunkSize", chunkSize).Msg("querying NTP servers")
// shuffle the ntp servers to avoid always querying the same servers // shuffle the ntp servers to avoid always querying the same servers
rand.Shuffle(len(ntpServers), func(i, j int) { ntpServers[i], ntpServers[j] = ntpServers[j], ntpServers[i] }) rand.Shuffle(len(ntpServers), func(i, j int) { ntpServers[i], ntpServers[j] = ntpServers[j], ntpServers[i] })
@ -46,6 +47,10 @@ type ntpResult struct {
func (t *TimeSync) queryMultipleNTP(servers []string, timeout time.Duration) (now *time.Time, offset *time.Duration) { func (t *TimeSync) queryMultipleNTP(servers []string, timeout time.Duration) (now *time.Time, offset *time.Duration) {
results := make(chan *ntpResult, len(servers)) results := make(chan *ntpResult, len(servers))
_, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
for _, server := range servers { for _, server := range servers {
go func(server string) { go func(server string) {
scopedLogger := t.l.With(). scopedLogger := t.l.With().
@ -66,15 +71,25 @@ func (t *TimeSync) queryMultipleNTP(servers []string, timeout time.Duration) (no
return return
} }
if response.IsKissOfDeath() {
scopedLogger.Warn().
Str("kiss_code", response.KissCode).
Msg("ignoring NTP server kiss of death")
results <- nil
return
}
rtt := float64(response.RTT.Milliseconds())
// set the last RTT // set the last RTT
metricNtpServerLastRTT.WithLabelValues( metricNtpServerLastRTT.WithLabelValues(
server, server,
).Set(float64(response.RTT.Milliseconds())) ).Set(rtt)
// set the RTT histogram // set the RTT histogram
metricNtpServerRttHistogram.WithLabelValues( metricNtpServerRttHistogram.WithLabelValues(
server, server,
).Observe(float64(response.RTT.Milliseconds())) ).Observe(rtt)
// set the server info // set the server info
metricNtpServerInfo.WithLabelValues( metricNtpServerInfo.WithLabelValues(
@ -91,10 +106,13 @@ func (t *TimeSync) queryMultipleNTP(servers []string, timeout time.Duration) (no
scopedLogger.Info(). scopedLogger.Info().
Str("time", now.Format(time.RFC3339)). Str("time", now.Format(time.RFC3339)).
Str("reference", response.ReferenceString()). Str("reference", response.ReferenceString()).
Str("rtt", response.RTT.String()). Float64("rtt", rtt).
Str("clockOffset", response.ClockOffset.String()). Str("clockOffset", response.ClockOffset.String()).
Uint8("stratum", response.Stratum). Uint8("stratum", response.Stratum).
Msg("NTP server returned time") Msg("NTP server returned time")
cancel()
results <- &ntpResult{ results <- &ntpResult{
now: now, now: now,
offset: &response.ClockOffset, offset: &response.ClockOffset,

View File

@ -28,9 +28,8 @@ type TimeSync struct {
syncLock *sync.Mutex syncLock *sync.Mutex
l *zerolog.Logger l *zerolog.Logger
ntpServers []string networkConfig *network.NetworkConfig
httpUrls []string dhcpNtpAddresses []string
networkConfig *network.NetworkConfig
rtcDevicePath string rtcDevicePath string
rtcDevice *os.File //nolint:unused rtcDevice *os.File //nolint:unused
@ -64,14 +63,13 @@ func NewTimeSync(opts *TimeSyncOptions) *TimeSync {
} }
t := &TimeSync{ t := &TimeSync{
syncLock: &sync.Mutex{}, syncLock: &sync.Mutex{},
l: opts.Logger, l: opts.Logger,
rtcDevicePath: rtcDevice, dhcpNtpAddresses: []string{},
rtcLock: &sync.Mutex{}, rtcDevicePath: rtcDevice,
preCheckFunc: opts.PreCheckFunc, rtcLock: &sync.Mutex{},
ntpServers: defaultNTPServers, preCheckFunc: opts.PreCheckFunc,
httpUrls: defaultHTTPUrls, networkConfig: opts.NetworkConfig,
networkConfig: opts.NetworkConfig,
} }
if t.rtcDevicePath != "" { if t.rtcDevicePath != "" {
@ -82,34 +80,42 @@ func NewTimeSync(opts *TimeSyncOptions) *TimeSync {
return t return t
} }
func (t *TimeSync) SetDhcpNtpAddresses(addresses []string) {
t.dhcpNtpAddresses = addresses
}
func (t *TimeSync) getSyncMode() SyncMode { func (t *TimeSync) getSyncMode() SyncMode {
syncMode := SyncMode{ syncMode := SyncMode{
Ntp: true,
Http: true,
Ordering: []string{"ntp_dhcp", "ntp", "http"},
NtpUseFallback: true, NtpUseFallback: true,
HttpUseFallback: true, HttpUseFallback: true,
} }
var syncModeString string
if t.networkConfig != nil { if t.networkConfig != nil {
syncModeString = t.networkConfig.TimeSyncMode.String switch t.networkConfig.TimeSyncMode.String {
case "ntp_only":
syncMode.Http = false
case "http_only":
syncMode.Ntp = false
}
if t.networkConfig.TimeSyncDisableFallback.Bool { if t.networkConfig.TimeSyncDisableFallback.Bool {
syncMode.NtpUseFallback = false syncMode.NtpUseFallback = false
syncMode.HttpUseFallback = false syncMode.HttpUseFallback = false
} }
var syncOrdering = t.networkConfig.TimeSyncOrdering
if len(syncOrdering) > 0 {
syncMode.Ordering = syncOrdering
}
} }
switch syncModeString { t.l.Debug().Strs("Ordering", syncMode.Ordering).Bool("Ntp", syncMode.Ntp).Bool("Http", syncMode.Http).Bool("NtpUseFallback", syncMode.NtpUseFallback).Bool("HttpUseFallback", syncMode.HttpUseFallback).Msg("sync mode")
case "ntp_only":
syncMode.Ntp = true
case "http_only":
syncMode.Http = true
default:
syncMode.Ntp = true
syncMode.Http = true
}
return syncMode return syncMode
} }
func (t *TimeSync) doTimeSync() { func (t *TimeSync) doTimeSync() {
metricTimeSyncStatus.Set(0) metricTimeSyncStatus.Set(0)
for { for {
@ -154,16 +160,61 @@ func (t *TimeSync) Sync() error {
offset *time.Duration offset *time.Duration
) )
syncMode := t.getSyncMode()
metricTimeSyncCount.Inc() metricTimeSyncCount.Inc()
if syncMode.Ntp { syncMode := t.getSyncMode()
now, offset = t.queryNetworkTime()
}
if syncMode.Http && now == nil { Orders:
now = t.queryAllHttpTime() for _, mode := range syncMode.Ordering {
switch mode {
case "ntp_user_provided":
if syncMode.Ntp {
t.l.Info().Msg("using NTP custom servers")
now, offset = t.queryNetworkTime(t.networkConfig.TimeSyncNTPServers)
if now != nil {
t.l.Info().Str("source", "NTP").Time("now", *now).Msg("time obtained")
break Orders
}
}
case "ntp_dhcp":
if syncMode.Ntp {
t.l.Info().Msg("using NTP servers from DHCP")
now, offset = t.queryNetworkTime(t.dhcpNtpAddresses)
if now != nil {
t.l.Info().Str("source", "NTP DHCP").Time("now", *now).Msg("time obtained")
break Orders
}
}
case "ntp":
if syncMode.Ntp && syncMode.NtpUseFallback {
t.l.Info().Msg("using NTP fallback")
now, offset = t.queryNetworkTime(defaultNTPServers)
if now != nil {
t.l.Info().Str("source", "NTP fallback").Time("now", *now).Msg("time obtained")
break Orders
}
}
case "http_user_provided":
if syncMode.Http {
t.l.Info().Msg("using HTTP custom URLs")
now = t.queryAllHttpTime(t.networkConfig.TimeSyncHTTPUrls)
if now != nil {
t.l.Info().Str("source", "HTTP").Time("now", *now).Msg("time obtained")
break Orders
}
}
case "http":
if syncMode.Http && syncMode.HttpUseFallback {
t.l.Info().Msg("using HTTP fallback")
now = t.queryAllHttpTime(defaultHTTPUrls)
if now != nil {
t.l.Info().Str("source", "HTTP fallback").Time("now", *now).Msg("time obtained")
break Orders
}
}
default:
t.l.Warn().Str("mode", mode).Msg("unknown time sync mode, skipping")
}
} }
if now == nil { if now == nil {

View File

@ -19,6 +19,14 @@ func networkStateChanged() {
// do not block the main thread // do not block the main thread
go waitCtrlAndRequestDisplayUpdate(true) go waitCtrlAndRequestDisplayUpdate(true)
if timeSync != nil {
if networkState != nil {
timeSync.SetDhcpNtpAddresses(networkState.NtpAddressesString())
}
timeSync.Sync()
}
// always restart mDNS when the network state changes // always restart mDNS when the network state changes
if mDNS != nil { if mDNS != nil {
_ = mDNS.SetListenOptions(config.NetworkConfig.GetMDNSMode()) _ = mDNS.SetListenOptions(config.NetworkConfig.GetMDNSMode())

1385
ui/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -19,21 +19,21 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@headlessui/react": "^2.2.3", "@headlessui/react": "^2.2.4",
"@headlessui/tailwindcss": "^0.2.2", "@headlessui/tailwindcss": "^0.2.2",
"@heroicons/react": "^2.2.0", "@heroicons/react": "^2.2.0",
"@vitejs/plugin-basic-ssl": "^2.0.0", "@vitejs/plugin-basic-ssl": "^2.1.0",
"@xterm/addon-clipboard": "^0.1.0", "@xterm/addon-clipboard": "^0.1.0",
"@xterm/addon-fit": "^0.10.0", "@xterm/addon-fit": "^0.10.0",
"@xterm/addon-unicode11": "^0.8.0", "@xterm/addon-unicode11": "^0.8.0",
"@xterm/addon-web-links": "^0.11.0", "@xterm/addon-web-links": "^0.11.0",
"@xterm/addon-webgl": "^0.18.0", "@xterm/addon-webgl": "^0.18.0",
"@xterm/xterm": "^5.5.0", "@xterm/xterm": "^5.5.0",
"cva": "^1.0.0-beta.3", "cva": "^1.0.0-beta.4",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"eslint-import-resolver-alias": "^1.1.2", "eslint-import-resolver-alias": "^1.1.2",
"focus-trap-react": "^11.0.3", "focus-trap-react": "^11.0.4",
"framer-motion": "^12.11.4", "framer-motion": "^12.23.0",
"lodash.throttle": "^4.1.1", "lodash.throttle": "^4.1.1",
"mini-svg-data-uri": "^1.4.4", "mini-svg-data-uri": "^1.4.4",
"react": "^19.1.0", "react": "^19.1.0",
@ -42,42 +42,42 @@
"react-hot-toast": "^2.5.2", "react-hot-toast": "^2.5.2",
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
"react-router-dom": "^6.22.3", "react-router-dom": "^6.22.3",
"react-simple-keyboard": "^3.8.72", "react-simple-keyboard": "^3.8.89",
"react-use-websocket": "^4.13.0", "react-use-websocket": "^4.13.0",
"react-xtermjs": "^1.0.10", "react-xtermjs": "^1.0.10",
"recharts": "^2.15.3", "recharts": "^2.15.3",
"tailwind-merge": "^3.3.0", "tailwind-merge": "^3.3.1",
"usehooks-ts": "^3.1.1", "usehooks-ts": "^3.1.1",
"validator": "^13.15.0", "validator": "^13.15.15",
"zustand": "^4.5.2" "zustand": "^4.5.2"
}, },
"devDependencies": { "devDependencies": {
"@eslint/compat": "^1.2.9", "@eslint/compat": "^1.3.1",
"@eslint/eslintrc": "^3.3.1", "@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.26.0", "@eslint/js": "^9.30.1",
"@tailwindcss/forms": "^0.5.10", "@tailwindcss/forms": "^0.5.10",
"@tailwindcss/postcss": "^4.1.7", "@tailwindcss/postcss": "^4.1.11",
"@tailwindcss/typography": "^0.5.16", "@tailwindcss/typography": "^0.5.16",
"@tailwindcss/vite": "^4.1.7", "@tailwindcss/vite": "^4.1.11",
"@types/react": "^19.1.4", "@types/react": "^19.1.8",
"@types/react-dom": "^19.1.5", "@types/react-dom": "^19.1.6",
"@types/semver": "^7.7.0", "@types/semver": "^7.7.0",
"@types/validator": "^13.15.0", "@types/validator": "^13.15.2",
"@typescript-eslint/eslint-plugin": "^8.32.1", "@typescript-eslint/eslint-plugin": "^8.35.1",
"@typescript-eslint/parser": "^8.32.1", "@typescript-eslint/parser": "^8.35.1",
"@vitejs/plugin-react-swc": "^3.9.0", "@vitejs/plugin-react-swc": "^3.10.2",
"autoprefixer": "^10.4.21", "autoprefixer": "^10.4.21",
"eslint": "^9.26.0", "eslint": "^9.30.1",
"eslint-config-prettier": "^10.1.5", "eslint-config-prettier": "^10.1.5",
"eslint-plugin-import": "^2.31.0", "eslint-plugin-import": "^2.32.0",
"eslint-plugin-react": "^7.37.5", "eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20", "eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.1.0", "globals": "^16.3.0",
"postcss": "^8.5.3", "postcss": "^8.5.6",
"prettier": "^3.5.3", "prettier": "^3.6.2",
"prettier-plugin-tailwindcss": "^0.6.11", "prettier-plugin-tailwindcss": "^0.6.13",
"tailwindcss": "^4.1.7", "tailwindcss": "^4.1.11",
"typescript": "^5.8.3", "typescript": "^5.8.3",
"vite": "^6.3.5", "vite": "^6.3.5",
"vite-tsconfig-paths": "^5.1.4" "vite-tsconfig-paths": "^5.1.4"

View File

@ -40,6 +40,12 @@ export default function InfoBar() {
const keyboardLedState = useHidStore(state => state.keyboardLedState); const keyboardLedState = useHidStore(state => state.keyboardLedState);
const keyboardLedStateSyncAvailable = useHidStore(state => state.keyboardLedStateSyncAvailable); const keyboardLedStateSyncAvailable = useHidStore(state => state.keyboardLedStateSyncAvailable);
const keyboardLedSync = useSettingsStore(state => state.keyboardLedSync); const keyboardLedSync = useSettingsStore(state => state.keyboardLedSync);
const isShiftActive = useHidStore(state => state.isShiftActive);
const isCtrlActive = useHidStore(state => state.isCtrlActive);
const isAltActive = useHidStore(state => state.isAltActive);
const isMetaActive = useHidStore(state => state.isMetaActive);
const isAltGrActive = useHidStore(state => state.isAltGrActive);
const isTurnServerInUse = useRTCStore(state => state.isTurnServerInUse); const isTurnServerInUse = useRTCStore(state => state.isTurnServerInUse);
@ -135,6 +141,56 @@ export default function InfoBar() {
{keyboardLedSync === "browser" ? "Browser" : "Host"} {keyboardLedSync === "browser" ? "Browser" : "Host"}
</div> </div>
) : null} ) : null}
<div
className={cx(
"shrink-0 p-1 px-1.5 text-xs",
isShiftActive
? "text-black dark:text-white"
: "text-slate-800/20 dark:text-slate-300/20",
)}
>
Shift
</div>
<div
className={cx(
"shrink-0 p-1 px-1.5 text-xs",
isCtrlActive
? "text-black dark:text-white"
: "text-slate-800/20 dark:text-slate-300/20",
)}
>
Ctrl
</div>
<div
className={cx(
"shrink-0 p-1 px-1.5 text-xs",
isAltActive
? "text-black dark:text-white"
: "text-slate-800/20 dark:text-slate-300/20",
)}
>
Alt
</div>
<div
className={cx(
"shrink-0 p-1 px-1.5 text-xs",
isMetaActive
? "text-black dark:text-white"
: "text-slate-800/20 dark:text-slate-300/20",
)}
>
Meta
</div>
<div
className={cx(
"shrink-0 p-1 px-1.5 text-xs",
isAltGrActive
? "text-black dark:text-white"
: "text-slate-800/20 dark:text-slate-300/20",
)}
>
AltGr
</div>
<div <div
className={cx( className={cx(
"shrink-0 p-1 px-1.5 text-xs", "shrink-0 p-1 px-1.5 text-xs",

View File

@ -27,6 +27,7 @@ const AttachIcon = ({ className }: { className?: string }) => {
function KeyboardWrapper() { function KeyboardWrapper() {
const [layoutName, setLayoutName] = useState("default"); const [layoutName, setLayoutName] = useState("default");
const [depressedButtons, setDepressedButtons] = useState("");
const keyboardRef = useRef<HTMLDivElement>(null); const keyboardRef = useRef<HTMLDivElement>(null);
const showAttachedVirtualKeyboard = useUiStore( const showAttachedVirtualKeyboard = useUiStore(
@ -54,6 +55,21 @@ function KeyboardWrapper() {
const setIsCapsLockActive = useHidStore(state => state.setIsCapsLockActive); const setIsCapsLockActive = useHidStore(state => state.setIsCapsLockActive);
const isShiftActive = useHidStore(state => state.isShiftActive);
const setIsShiftActive = useHidStore(state => state.setIsShiftActive);
const isCtrlActive = useHidStore(state => state.isCtrlActive);
const setIsCtrlActive = useHidStore(state => state.setIsCtrlActive);
const isAltActive = useHidStore(state => state.isAltActive);
const setIsAltActive = useHidStore(state => state.setIsAltActive);
const isMetaActive = useHidStore(state => state.isMetaActive);
const setIsMetaActive = useHidStore(state => state.setIsMetaActive);
const isAltGrActive = useHidStore(state => state.isAltGrActive);
const setIsAltGrActive = useHidStore(state => state.setIsAltGrActive);
const startDrag = useCallback((e: MouseEvent | TouchEvent) => { const startDrag = useCallback((e: MouseEvent | TouchEvent) => {
if (!keyboardRef.current) return; if (!keyboardRef.current) return;
if (e instanceof TouchEvent && e.touches.length > 1) return; if (e instanceof TouchEvent && e.touches.length > 1) return;
@ -123,80 +139,123 @@ function KeyboardWrapper() {
}; };
}, [endDrag, onDrag, startDrag]); }, [endDrag, onDrag, startDrag]);
const onKeyDown = useCallback( useEffect(() => {
(key: string) => { // if you have the CapsLock "down", then the shift state is inverted
const isKeyShift = key === "{shift}" || key === "ShiftLeft" || key === "ShiftRight"; const effectiveShift = isCapsLockActive ? false === isShiftActive : isShiftActive;
const isKeyCaps = key === "CapsLock"; setLayoutName(effectiveShift ? "shift" : "default");
const cleanKey = key.replace(/[()]/g, ""); },
const keyHasShiftModifier = key.includes("("); [setLayoutName, isCapsLockActive, isShiftActive]
);
// Handle toggle of layout for shift or caps lock // this causes the buttons to look depressed/clicked depending on the sticky state
const toggleLayout = () => { useEffect(() => {
setLayoutName(prevLayout => (prevLayout === "default" ? "shift" : "default")); let buttons = "None "; // make sure we name at least one (fake) button
}; if (isCapsLockActive) buttons += "CapsLock ";
if (isShiftActive) buttons += "ShiftLeft ShiftRight ";
if (isCtrlActive) buttons += "ControlLeft ControlRight ";
if (isAltActive) buttons += "AltLeft AltRight ";
if (isMetaActive) buttons += "MetaLeft MetaRight ";
setDepressedButtons(buttons.trimEnd());
},
[setDepressedButtons, isCapsLockActive, isShiftActive, isCtrlActive, isAltActive, isMetaActive, isAltGrActive]
);
if (key === "CtrlAltDelete") { const onKeyPress = useCallback((key: string) => {
sendKeyboardEvent( // handle the fake combo keys first
[keys["Delete"]], if (key === "CtrlAltDelete") {
[modifiers["ControlLeft"], modifiers["AltLeft"]], sendKeyboardEvent(
); [keys["Delete"]],
setTimeout(resetKeyboardState, 100); [modifiers["ControlLeft"], modifiers["AltLeft"]],
return; );
} setTimeout(resetKeyboardState, 100);
return;
}
if (key === "AltMetaEscape") { if (key === "AltMetaEscape") {
sendKeyboardEvent( sendKeyboardEvent(
[keys["Escape"]], [keys["Escape"]],
[modifiers["MetaLeft"], modifiers["AltLeft"]], [modifiers["MetaLeft"], modifiers["AltLeft"]],
); );
setTimeout(resetKeyboardState, 100);
return;
}
if (key === "CtrlAltBackspace") {
sendKeyboardEvent(
[keys["Backspace"]],
[modifiers["ControlLeft"], modifiers["AltLeft"]],
);
setTimeout(resetKeyboardState, 100);
return;
}
if (isKeyShift || isKeyCaps) {
toggleLayout();
if (isCapsLockActive) {
if (!isKeyboardLedManagedByHost) {
setIsCapsLockActive(false);
}
sendKeyboardEvent([keys["CapsLock"]], []);
return;
}
}
// Handle caps lock state change
if (isKeyCaps && !isKeyboardLedManagedByHost) {
setIsCapsLockActive(!isCapsLockActive);
}
// Collect new active keys and modifiers
const newKeys = keys[cleanKey] ? [keys[cleanKey]] : [];
const newModifiers =
keyHasShiftModifier && !isCapsLockActive ? [modifiers["ShiftLeft"]] : [];
// Update current keys and modifiers
sendKeyboardEvent(newKeys, newModifiers);
// If shift was used as a modifier and caps lock is not active, revert to default layout
if (keyHasShiftModifier && !isCapsLockActive) {
setLayoutName("default");
}
setTimeout(resetKeyboardState, 100); setTimeout(resetKeyboardState, 100);
}, return;
[isCapsLockActive, isKeyboardLedManagedByHost, sendKeyboardEvent, resetKeyboardState, setIsCapsLockActive], }
if (key === "CtrlAltBackspace") {
sendKeyboardEvent(
[keys["Backspace"]],
[modifiers["ControlLeft"], modifiers["AltLeft"]],
);
setTimeout(resetKeyboardState, 100);
return;
}
// strip away the parens for shifted characters
const cleanKey = key.replace(/[()]/g, "");
const passthrough = ["PrintScreen", "SystemRequest", "Pause", "Break", "ScrollLock", "Enter", "Space"].find((value) => value === cleanKey);
if (passthrough) {
emitkeycode(cleanKey);
return;
}
// adjust the sticky state of the Shift/Ctrl/Alt/Meta/AltGr
if (key === "CapsLock" && !isKeyboardLedManagedByHost)
setIsCapsLockActive(!isCapsLockActive);
else if (key === "ShiftLeft" || key === "ShiftRight")
setIsShiftActive(!isShiftActive);
else if (key === "ControlLeft" || key === "ControlRight")
setIsCtrlActive(!isCtrlActive);
else if (key === "AltLeft" || key === "AltRight")
setIsAltActive(!isAltActive);
else if (key === "MetaLeft" || key === "MetaRight")
setIsMetaActive(!isMetaActive);
else if (key === "AltGr")
setIsAltGrActive(!isAltGrActive);
emitkeycode(cleanKey);
function emitkeycode(key: string) {
const effectiveMods: number[] = [];
if (isShiftActive)
effectiveMods.push(modifiers["ShiftLeft"]);
if (isCtrlActive)
effectiveMods.push(modifiers["ControlLeft"]);
if (isAltActive)
effectiveMods.push(modifiers["AltLeft"]);
if (isMetaActive)
effectiveMods.push(modifiers["MetaLeft"]);
if (isAltGrActive) {
effectiveMods.push(modifiers["MetaRight"]);
effectiveMods.push(modifiers["CtrlLeft"]);
}
const keycode = keys[key];
if (keycode) {
// send the keycode with modifiers
sendKeyboardEvent([keycode], effectiveMods);
}
// release the key (if one pressed), but retain the modifiers
setTimeout(() => sendKeyboardEvent([], effectiveMods), 50);
}
},
[isKeyboardLedManagedByHost,
setIsCapsLockActive, isCapsLockActive,
setIsShiftActive, isShiftActive,
setIsCtrlActive, isCtrlActive,
setIsAltActive, isAltActive,
setIsMetaActive, isMetaActive,
setIsAltGrActive, isAltGrActive,
sendKeyboardEvent, resetKeyboardState
],
); );
const virtualKeyboard = useHidStore(state => state.isVirtualKeyboardEnabled); const virtualKeyboard = useHidStore(state => state.isVirtualKeyboardEnabled);
@ -276,12 +335,16 @@ function KeyboardWrapper() {
<Keyboard <Keyboard
baseClass="simple-keyboard-main" baseClass="simple-keyboard-main"
layoutName={layoutName} layoutName={layoutName}
onKeyPress={onKeyDown} onKeyPress={onKeyPress}
buttonTheme={[ buttonTheme={[
{ {
class: "combination-key", class: "combination-key",
buttons: "CtrlAltDelete AltMetaEscape CtrlAltBackspace", buttons: "CtrlAltDelete AltMetaEscape CtrlAltBackspace",
}, },
{
class: "depressed-key",
buttons: depressedButtons
},
]} ]}
display={keyDisplayMap} display={keyDisplayMap}
layout={{ layout={{
@ -305,8 +368,6 @@ function KeyboardWrapper() {
], ],
}} }}
disableButtonHold={true} disableButtonHold={true}
syncInstanceInputs={true}
debug={false}
/> />
<div className="controlArrows"> <div className="controlArrows">
@ -314,25 +375,24 @@ function KeyboardWrapper() {
baseClass="simple-keyboard-control" baseClass="simple-keyboard-control"
theme="simple-keyboard hg-theme-default hg-layout-default" theme="simple-keyboard hg-theme-default hg-layout-default"
layoutName={layoutName} layoutName={layoutName}
onKeyPress={onKeyDown} onKeyPress={onKeyPress}
display={keyDisplayMap} display={keyDisplayMap}
layout={{ layout={{
default: ["PrintScreen ScrollLock Pause", "Insert Home Pageup", "Delete End Pagedown"], default: ["PrintScreen ScrollLock Pause", "Insert Home PageUp", "Delete End PageDown"],
shift: ["(PrintScreen) ScrollLock (Pause)", "Insert Home Pageup", "Delete End Pagedown"], shift: ["(PrintScreen) ScrollLock (Pause)", "Insert Home PageUp", "Delete End PageDown"],
}} }}
syncInstanceInputs={true} disableButtonHold={true}
debug={false}
/> />
<Keyboard <Keyboard
baseClass="simple-keyboard-arrows" baseClass="simple-keyboard-arrows"
theme="simple-keyboard hg-theme-default hg-layout-default" theme="simple-keyboard hg-theme-default hg-layout-default"
onKeyPress={onKeyDown} onKeyPress={onKeyPress}
display={keyDisplayMap} display={keyDisplayMap}
layout={{ layout={{
default: ["ArrowUp", "ArrowLeft ArrowDown ArrowRight"], default: ["ArrowUp", "ArrowLeft ArrowDown ArrowRight"],
shift: ["ArrowUp", "ArrowLeft ArrowDown ArrowRight"],
}} }}
syncInstanceInputs={true} disableButtonHold={true}
debug={false}
/> />
</div> </div>
</div> </div>

View File

@ -71,6 +71,15 @@ export default function WebRTCVideo() {
const hdmiError = ["no_lock", "no_signal", "out_of_range"].includes(hdmiState); const hdmiError = ["no_lock", "no_signal", "out_of_range"].includes(hdmiState);
const isVideoLoading = !isPlaying; const isVideoLoading = !isPlaying;
// Keyboard related states
const {
setIsShiftActive,
setIsCtrlActive,
setIsAltActive,
setIsMetaActive,
setIsAltGrActive
} = useHidStore();
const [blockWheelEvent, setBlockWheelEvent] = useState(false); const [blockWheelEvent, setBlockWheelEvent] = useState(false);
// Misc states and hooks // Misc states and hooks
@ -423,6 +432,12 @@ export default function WebRTCVideo() {
setIsScrollLockActive(e.getModifierState("ScrollLock")); setIsScrollLockActive(e.getModifierState("ScrollLock"));
} }
setIsShiftActive(e.getModifierState("Shift"))
setIsCtrlActive(e.getModifierState("Control"))
setIsAltActive(e.getModifierState("Alt"))
setIsMetaActive(e.getModifierState("Meta"))
setIsAltGrActive(e.getModifierState("AltGraph"))
if (code == "IntlBackslash" && ["`", "~"].includes(key)) { if (code == "IntlBackslash" && ["`", "~"].includes(key)) {
code = "Backquote"; code = "Backquote";
} else if (code == "Backquote" && ["§", "±"].includes(key)) { } else if (code == "Backquote" && ["§", "±"].includes(key)) {
@ -452,12 +467,17 @@ export default function WebRTCVideo() {
sendKeyboardEvent([...new Set(newKeys)], [...new Set(newModifiers)]); sendKeyboardEvent([...new Set(newKeys)], [...new Set(newModifiers)]);
}, },
[ [
handleModifierKeys,
sendKeyboardEvent,
isKeyboardLedManagedByHost, isKeyboardLedManagedByHost,
setIsNumLockActive, setIsNumLockActive,
setIsCapsLockActive, setIsCapsLockActive,
setIsScrollLockActive, setIsScrollLockActive,
setIsShiftActive,
setIsCtrlActive,
setIsAltActive,
setIsMetaActive,
setIsAltGrActive,
handleModifierKeys,
sendKeyboardEvent
], ],
); );
@ -472,6 +492,12 @@ export default function WebRTCVideo() {
setIsScrollLockActive(e.getModifierState("ScrollLock")); setIsScrollLockActive(e.getModifierState("ScrollLock"));
} }
setIsShiftActive(e.getModifierState("Shift"))
setIsCtrlActive(e.getModifierState("Control"))
setIsAltActive(e.getModifierState("Alt"))
setIsMetaActive(e.getModifierState("Meta"))
setIsAltGrActive(e.getModifierState("AltGraph"))
// Filtering out the key that was just released (keys[e.code]) // Filtering out the key that was just released (keys[e.code])
const newKeys = prev.activeKeys.filter(k => k !== keys[e.code]).filter(Boolean); const newKeys = prev.activeKeys.filter(k => k !== keys[e.code]).filter(Boolean);
@ -484,12 +510,17 @@ export default function WebRTCVideo() {
sendKeyboardEvent([...new Set(newKeys)], [...new Set(newModifiers)]); sendKeyboardEvent([...new Set(newKeys)], [...new Set(newModifiers)]);
}, },
[ [
handleModifierKeys,
sendKeyboardEvent,
isKeyboardLedManagedByHost, isKeyboardLedManagedByHost,
setIsNumLockActive, setIsNumLockActive,
setIsCapsLockActive, setIsCapsLockActive,
setIsScrollLockActive, setIsScrollLockActive,
setIsShiftActive,
setIsCtrlActive,
setIsAltActive,
setIsMetaActive,
setIsAltGrActive,
handleModifierKeys,
sendKeyboardEvent
], ],
); );
@ -657,6 +688,16 @@ export default function WebRTCVideo() {
return true; return true;
}, [isPlaying, isPointerLockActive, isPointerLockPossible, isVideoLoading, settings.mouseMode, videoHeight, videoWidth]); }, [isPlaying, isPointerLockActive, isPointerLockPossible, isVideoLoading, settings.mouseMode, videoHeight, videoWidth]);
// Conditionally set the filter style so we don't fallback to software rendering if these values are default of 1.0
const videoStyle = useMemo(() => {
const isDefault = videoSaturation === 1.0 && videoBrightness === 1.0 && videoContrast === 1.0;
return isDefault
? {} // No filter if all settings are default (1.0)
: {
filter: `saturate(${videoSaturation}) brightness(${videoBrightness}) contrast(${videoContrast})`,
};
}, [videoSaturation, videoBrightness, videoContrast]);
return ( return (
<div className="grid h-full w-full grid-rows-(--grid-layout)"> <div className="grid h-full w-full grid-rows-(--grid-layout)">
<div className="flex min-h-[39.5px] flex-col"> <div className="flex min-h-[39.5px] flex-col">
@ -691,17 +732,15 @@ export default function WebRTCVideo() {
<div className="relative flex h-full w-full items-center justify-center"> <div className="relative flex h-full w-full items-center justify-center">
<video <video
ref={videoElm} ref={videoElm}
autoPlay={true} autoPlay
controls={false} controls={false}
onPlaying={onVideoPlaying} onPlaying={onVideoPlaying}
onPlay={onVideoPlaying} onPlay={onVideoPlaying}
muted={true} muted
playsInline playsInline
disablePictureInPicture disablePictureInPicture
controlsList="nofullscreen" controlsList="nofullscreen"
style={{ style={videoStyle}
filter: `saturate(${videoSaturation}) brightness(${videoBrightness}) contrast(${videoContrast})`,
}}
className={cx( className={cx(
"max-h-full min-h-[384px] max-w-full min-w-[512px] bg-black/50 object-contain transition-all duration-1000", "max-h-full min-h-[384px] max-w-full min-w-[512px] bg-black/50 object-contain transition-all duration-1000",
{ {

View File

@ -475,6 +475,21 @@ export interface HidState {
isVirtualKeyboardEnabled: boolean; isVirtualKeyboardEnabled: boolean;
setVirtualKeyboardEnabled: (enabled: boolean) => void; setVirtualKeyboardEnabled: (enabled: boolean) => void;
isShiftActive: boolean;
setIsShiftActive: (enabled: boolean) => void;
isCtrlActive: boolean;
setIsCtrlActive: (enabled: boolean) => void;
isAltActive: boolean;
setIsAltActive: (enabled: boolean) => void;
isMetaActive: boolean;
setIsMetaActive: (enabled: boolean) => void;
isAltGrActive: boolean;
setIsAltGrActive: (enabled: boolean) => void;
isPasteModeEnabled: boolean; isPasteModeEnabled: boolean;
setPasteModeEnabled: (enabled: boolean) => void; setPasteModeEnabled: (enabled: boolean) => void;
@ -521,6 +536,21 @@ export const useHidStore = create<HidState>((set, get) => ({
isVirtualKeyboardEnabled: false, isVirtualKeyboardEnabled: false,
setVirtualKeyboardEnabled: enabled => set({ isVirtualKeyboardEnabled: enabled }), setVirtualKeyboardEnabled: enabled => set({ isVirtualKeyboardEnabled: enabled }),
isShiftActive: false,
setIsShiftActive: enabled => set({ isShiftActive: enabled }),
isCtrlActive: false,
setIsCtrlActive: enabled => set({ isCtrlActive: enabled }),
isAltActive: false,
setIsAltActive: enabled => set({ isAltActive: enabled }),
isMetaActive: false,
setIsMetaActive: enabled => set({ isMetaActive: enabled }),
isAltGrActive: false,
setIsAltGrActive: enabled => set({ isAltGrActive: enabled }),
isPasteModeEnabled: false, isPasteModeEnabled: false,
setPasteModeEnabled: enabled => set({ isPasteModeEnabled: enabled }), setPasteModeEnabled: enabled => set({ isPasteModeEnabled: enabled }),

View File

@ -222,6 +222,10 @@ video::-webkit-media-controls {
background: none; background: none;
} }
.simple-keyboard .hg-button.depressed-key {
@apply border-gray-800! border-b-gray-600! border-t-gray-900! bg-gray-700!;
}
.simple-keyboard .hg-button.selectedButton { .simple-keyboard .hg-button.selectedButton {
background: rgba(5, 25, 70, 0.53); background: rgba(5, 25, 70, 0.53);
@apply text-white; @apply text-white;

View File

@ -5,14 +5,14 @@ export const keys = {
ArrowLeft: 0x50, ArrowLeft: 0x50,
ArrowRight: 0x4f, ArrowRight: 0x4f,
ArrowUp: 0x52, ArrowUp: 0x52,
Backquote: 0x35, Backquote: 0x35, // aka Grave
Backslash: 0x31, Backslash: 0x31,
Backspace: 0x2a, Backspace: 0x2a,
BracketLeft: 0x2f, BracketLeft: 0x2f, // aka LeftBrace
BracketRight: 0x30, BracketRight: 0x30, // aka RightBrace
CapsLock: 0x39, CapsLock: 0x39,
Comma: 0x36, Comma: 0x36,
ContextMenu: 0, Compose: 0x65,
Delete: 0x4c, Delete: 0x4c,
Digit0: 0x27, Digit0: 0x27,
Digit1: 0x1e, Digit1: 0x1e,
@ -41,6 +41,18 @@ export const keys = {
F11: 0x44, F11: 0x44,
F12: 0x45, F12: 0x45,
F13: 0x68, F13: 0x68,
F14: 0x69,
F15: 0x6a,
F16: 0x6b,
F17: 0x6c,
F18: 0x6d,
F19: 0x6e,
F20: 0x6f,
F21: 0x70,
F22: 0x71,
F23: 0x72,
F24: 0x73,
HashTilde: 0x32,
Home: 0x4a, Home: 0x4a,
Insert: 0x49, Insert: 0x49,
IntlBackslash: 0x64, IntlBackslash: 0x64,
@ -70,37 +82,58 @@ export const keys = {
KeyX: 0x1b, KeyX: 0x1b,
KeyY: 0x1c, KeyY: 0x1c,
KeyZ: 0x1d, KeyZ: 0x1d,
KeypadComma: 0x85,
KeypadEqual: 0x86,
KeyRO: 0x87,
KatakanaHiragana: 0x88,
Yen: 0x89,
Henkan: 0x8a,
Muhenkan: 0x8b,
KPJPComma: 0x8c,
International7: 0x8d,
International8: 0x8e,
International9: 0x8f,
Hangeul: 0x90,
Hanja: 0x91,
Katakana: 0x92,
Hiragana: 0x93,
Zenkakuhankaku:0x94,
KeypadLeftParen: 0xb6,
KeypadRightParen: 0xb7,
KeypadExclamation: 0xcf, KeypadExclamation: 0xcf,
Minus: 0x2d, Minus: 0x2d,
NumLock: 0x53, None: 0x00,
Numpad0: 0x62, NumLock: 0x53, // and Clear
Numpad1: 0x59, Numpad0: 0x62, // and Insert
Numpad2: 0x5a, Numpad1: 0x59, // and End
Numpad3: 0x5b, Numpad2: 0x5a, // and Down Arrow
Numpad4: 0x5c, Numpad3: 0x5b, // and Page Down
Numpad4: 0x5c, // and Left Arrow
Numpad5: 0x5d, Numpad5: 0x5d,
Numpad6: 0x5e, Numpad6: 0x5e, // and Right Arrow
Numpad7: 0x5f, Numpad7: 0x5f, // and Home
Numpad8: 0x60, Numpad8: 0x60, // and Up Arrow
Numpad9: 0x61, Numpad9: 0x61, // and Page Up
NumpadAdd: 0x57, NumpadPlus: 0x57,
NumpadDivide: 0x54, NumpadSlash: 0x54,
NumpadEnter: 0x58, NumpadEnter: 0x58,
NumpadEqual: 0x67, NumpadEqual: 0x67,
NumpadMultiply: 0x55, NumpadAsterisk: 0x55,
NumpadSubtract: 0x56, NumpadMinus: 0x56,
NumpadDecimal: 0x63, NumpadDecimal: 0x63, // aka NumpadDot and Delete
Overflow: 0x01,
PageDown: 0x4e, PageDown: 0x4e,
PageUp: 0x4b, PageUp: 0x4b,
Period: 0x37, Period: 0x37, // aka Dot
PrintScreen: 0x46, PrintScreen: 0x46,
Pause: 0x48, Pause: 0x48,
Quote: 0x34, Power: 0x66,
Quote: 0x34, // aka Apostrophe
ScrollLock: 0x47, ScrollLock: 0x47,
Semicolon: 0x33, Semicolon: 0x33,
Slash: 0x38, Slash: 0x38,
Space: 0x2c, Space: 0x2c,
SystemRequest: 0x9a, SystemRequest: 0x9a, // aka Attention
Tab: 0x2b, Tab: 0x2b,
} as Record<string, number>; } as Record<string, number>;
@ -131,23 +164,23 @@ export const keyDisplayMap: Record<string, string> = {
AltMetaEscape: "Alt + Meta + Escape", AltMetaEscape: "Alt + Meta + Escape",
CtrlAltBackspace: "Ctrl + Alt + Backspace", CtrlAltBackspace: "Ctrl + Alt + Backspace",
Escape: "esc", Escape: "esc",
Tab: "tab", Tab: "tab ⇥",
Backspace: "backspace", Backspace: "backspace ⌫",
"(Backspace)": "backspace",
Enter: "enter", Enter: "enter",
CapsLock: "caps lock", CapsLock: "caps lock ⇪",
ShiftLeft: "shift", ShiftLeft: "shift ⇧",
ShiftRight: "shift", ShiftRight: "⇧ shift",
ControlLeft: "ctrl", ControlLeft: "ctrl ⌃",
AltLeft: "alt", ControlRight: "⌃ ctrl",
AltRight: "alt", AltLeft: "alt ⌥",
MetaLeft: "meta", AltRight: "⌥ alt",
MetaRight: "meta", MetaLeft: "meta ⌘", // "meta ⊞" for windows
MetaRight: "⌘ meta",// "≣ meta" for windows
Space: " ", Space: " ",
Insert: "insert", Insert: "ins",
Home: "home", Home: "home",
PageUp: "page up", PageUp: "page up",
Delete: "delete", Delete: "del",
End: "end", End: "end",
PageDown: "page down", PageDown: "page down",
ArrowLeft: "←", ArrowLeft: "←",
@ -213,13 +246,12 @@ export const keyDisplayMap: Record<string, string> = {
Numpad0: "Num 0", Numpad1: "Num 1", Numpad2: "Num 2", Numpad0: "Num 0", Numpad1: "Num 1", Numpad2: "Num 2",
Numpad3: "Num 3", Numpad4: "Num 4", Numpad5: "Num 5", Numpad3: "Num 3", Numpad4: "Num 4", Numpad5: "Num 5",
Numpad6: "Num 6", Numpad7: "Num 7", Numpad8: "Num 8", Numpad6: "Num 6", Numpad7: "Num 7", Numpad8: "Num 8",
Numpad9: "Num 9", NumpadAdd: "Num +", NumpadSubtract: "Num -", Numpad9: "Num 9", NumpadPlus: "Num +", NumpadMinus: "Num -",
NumpadMultiply: "Num *", NumpadDivide: "Num /", NumpadDecimal: "Num .", NumpadAsterisk: "Num *", NumpadSlash: "Num /", NumpadDecimal: "Num .",
NumpadEqual: "Num =", NumpadEnter: "Num Enter", NumpadEqual: "Num =", NumpadEnter: "Num Enter",
NumLock: "Num Lock", NumLock: "Num Lock",
// Modals // Modals
PrintScreen: "prt sc", ScrollLock: "scr lk", Pause: "pause", PrintScreen: "prt sc", ScrollLock: "scr lk", Pause: "pause",
"(PrintScreen)": "sys rq", "(Pause)": "break", "(PrintScreen)": "sys rq", "(Pause)": "break"
SystemRequest: "sys rq", Break: "break"
}; };