mirror of https://github.com/jetkvm/kvm.git
Compare commits
4 Commits
443aff5152
...
98d99561e4
Author | SHA1 | Date |
---|---|---|
|
98d99561e4 | |
|
4a23f22a55 | |
|
11a095c0f6 | |
|
84e4b44df0 |
|
@ -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) {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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] })
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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())
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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"
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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",
|
||||||
{
|
{
|
||||||
|
|
|
@ -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 }),
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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"
|
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue