mirror of https://github.com/jetkvm/kvm.git
Compare commits
4 Commits
9ffb614c3a
...
3537c19862
Author | SHA1 | Date |
---|---|---|
|
3537c19862 | |
|
4a23f22a55 | |
|
11a095c0f6 | |
|
a5b07b4862 |
|
@ -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"`
|
||||
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"`
|
||||
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"`
|
||||
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) {
|
||||
|
|
|
@ -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"`
|
||||
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"`
|
||||
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"`
|
||||
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 {
|
||||
|
|
|
@ -21,6 +21,7 @@ type NetworkInterfaceState struct {
|
|||
ipv6Addr *net.IP
|
||||
ipv6Addresses []IPv6Address
|
||||
ipv6LinkLocal *net.IP
|
||||
ntpAddresses []*net.IP
|
||||
macAddr *net.HardwareAddr
|
||||
|
||||
l *zerolog.Logger
|
||||
|
@ -76,6 +77,7 @@ func NewNetworkInterfaceState(opts *NetworkInterfaceOptions) (*NetworkInterfaceS
|
|||
onInitialCheck: opts.OnInitialCheck,
|
||||
cbConfigChange: opts.OnConfigChange,
|
||||
config: opts.NetworkConfig,
|
||||
ntpAddresses: make([]*net.IP, 0),
|
||||
}
|
||||
|
||||
// create the dhcp client
|
||||
|
@ -89,7 +91,7 @@ func NewNetworkInterfaceState(opts *NetworkInterfaceOptions) (*NetworkInterfaceS
|
|||
opts.Logger.Error().Err(err).Msg("failed to update network state")
|
||||
return
|
||||
}
|
||||
|
||||
_ = s.updateNtpServersFromLease(lease)
|
||||
_ = s.setHostnameIfNotSame()
|
||||
|
||||
opts.OnDhcpLeaseChange(lease)
|
||||
|
@ -135,6 +137,27 @@ func (s *NetworkInterfaceState) IPv6String() 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 {
|
||||
return s.macAddr
|
||||
}
|
||||
|
@ -318,6 +341,25 @@ func (s *NetworkInterfaceState) update() (DhcpTargetState, error) {
|
|||
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 {
|
||||
dhcpTargetState, err := s.update()
|
||||
if err != nil {
|
||||
|
|
|
@ -19,9 +19,9 @@ var defaultHTTPUrls = []string{
|
|||
// "http://www.msftconnecttest.com/connecttest.txt",
|
||||
}
|
||||
|
||||
func (t *TimeSync) queryAllHttpTime() (now *time.Time) {
|
||||
chunkSize := 4
|
||||
httpUrls := t.httpUrls
|
||||
func (t *TimeSync) queryAllHttpTime(httpUrls []string) (now *time.Time) {
|
||||
chunkSize := int(t.networkConfig.TimeSyncParallel.ValueOr(4))
|
||||
t.l.Info().Strs("httpUrls", httpUrls).Int("chunkSize", chunkSize).Msg("querying HTTP URLs")
|
||||
|
||||
// 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] })
|
||||
|
|
|
@ -73,6 +73,7 @@ var (
|
|||
},
|
||||
[]string{"url"},
|
||||
)
|
||||
|
||||
metricNtpServerInfo = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "jetkvm_timesync_ntp_server_info",
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package timesync
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/rand/v2"
|
||||
"strconv"
|
||||
"time"
|
||||
|
@ -21,9 +22,9 @@ var defaultNTPServers = []string{
|
|||
"3.pool.ntp.org",
|
||||
}
|
||||
|
||||
func (t *TimeSync) queryNetworkTime() (now *time.Time, offset *time.Duration) {
|
||||
chunkSize := 4
|
||||
ntpServers := t.ntpServers
|
||||
func (t *TimeSync) queryNetworkTime(ntpServers []string) (now *time.Time, offset *time.Duration) {
|
||||
chunkSize := int(t.networkConfig.TimeSyncParallel.ValueOr(4))
|
||||
t.l.Info().Strs("servers", ntpServers).Int("chunkSize", chunkSize).Msg("querying NTP 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] })
|
||||
|
@ -46,6 +47,10 @@ type ntpResult struct {
|
|||
|
||||
func (t *TimeSync) queryMultipleNTP(servers []string, timeout time.Duration) (now *time.Time, offset *time.Duration) {
|
||||
results := make(chan *ntpResult, len(servers))
|
||||
|
||||
_, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
for _, server := range servers {
|
||||
go func(server string) {
|
||||
scopedLogger := t.l.With().
|
||||
|
@ -66,15 +71,25 @@ func (t *TimeSync) queryMultipleNTP(servers []string, timeout time.Duration) (no
|
|||
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
|
||||
metricNtpServerLastRTT.WithLabelValues(
|
||||
server,
|
||||
).Set(float64(response.RTT.Milliseconds()))
|
||||
).Set(rtt)
|
||||
|
||||
// set the RTT histogram
|
||||
metricNtpServerRttHistogram.WithLabelValues(
|
||||
server,
|
||||
).Observe(float64(response.RTT.Milliseconds()))
|
||||
).Observe(rtt)
|
||||
|
||||
// set the server info
|
||||
metricNtpServerInfo.WithLabelValues(
|
||||
|
@ -91,10 +106,13 @@ func (t *TimeSync) queryMultipleNTP(servers []string, timeout time.Duration) (no
|
|||
scopedLogger.Info().
|
||||
Str("time", now.Format(time.RFC3339)).
|
||||
Str("reference", response.ReferenceString()).
|
||||
Str("rtt", response.RTT.String()).
|
||||
Float64("rtt", rtt).
|
||||
Str("clockOffset", response.ClockOffset.String()).
|
||||
Uint8("stratum", response.Stratum).
|
||||
Msg("NTP server returned time")
|
||||
|
||||
cancel()
|
||||
|
||||
results <- &ntpResult{
|
||||
now: now,
|
||||
offset: &response.ClockOffset,
|
||||
|
|
|
@ -28,9 +28,8 @@ type TimeSync struct {
|
|||
syncLock *sync.Mutex
|
||||
l *zerolog.Logger
|
||||
|
||||
ntpServers []string
|
||||
httpUrls []string
|
||||
networkConfig *network.NetworkConfig
|
||||
networkConfig *network.NetworkConfig
|
||||
dhcpNtpAddresses []string
|
||||
|
||||
rtcDevicePath string
|
||||
rtcDevice *os.File //nolint:unused
|
||||
|
@ -64,14 +63,13 @@ func NewTimeSync(opts *TimeSyncOptions) *TimeSync {
|
|||
}
|
||||
|
||||
t := &TimeSync{
|
||||
syncLock: &sync.Mutex{},
|
||||
l: opts.Logger,
|
||||
rtcDevicePath: rtcDevice,
|
||||
rtcLock: &sync.Mutex{},
|
||||
preCheckFunc: opts.PreCheckFunc,
|
||||
ntpServers: defaultNTPServers,
|
||||
httpUrls: defaultHTTPUrls,
|
||||
networkConfig: opts.NetworkConfig,
|
||||
syncLock: &sync.Mutex{},
|
||||
l: opts.Logger,
|
||||
dhcpNtpAddresses: []string{},
|
||||
rtcDevicePath: rtcDevice,
|
||||
rtcLock: &sync.Mutex{},
|
||||
preCheckFunc: opts.PreCheckFunc,
|
||||
networkConfig: opts.NetworkConfig,
|
||||
}
|
||||
|
||||
if t.rtcDevicePath != "" {
|
||||
|
@ -82,34 +80,42 @@ func NewTimeSync(opts *TimeSyncOptions) *TimeSync {
|
|||
return t
|
||||
}
|
||||
|
||||
func (t *TimeSync) SetDhcpNtpAddresses(addresses []string) {
|
||||
t.dhcpNtpAddresses = addresses
|
||||
}
|
||||
|
||||
func (t *TimeSync) getSyncMode() SyncMode {
|
||||
syncMode := SyncMode{
|
||||
Ntp: true,
|
||||
Http: true,
|
||||
Ordering: []string{"ntp_dhcp", "ntp", "http"},
|
||||
NtpUseFallback: true,
|
||||
HttpUseFallback: true,
|
||||
}
|
||||
var syncModeString string
|
||||
|
||||
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 {
|
||||
syncMode.NtpUseFallback = false
|
||||
syncMode.HttpUseFallback = false
|
||||
}
|
||||
|
||||
var syncOrdering = t.networkConfig.TimeSyncOrdering
|
||||
if len(syncOrdering) > 0 {
|
||||
syncMode.Ordering = syncOrdering
|
||||
}
|
||||
}
|
||||
|
||||
switch syncModeString {
|
||||
case "ntp_only":
|
||||
syncMode.Ntp = true
|
||||
case "http_only":
|
||||
syncMode.Http = true
|
||||
default:
|
||||
syncMode.Ntp = true
|
||||
syncMode.Http = true
|
||||
}
|
||||
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")
|
||||
|
||||
return syncMode
|
||||
}
|
||||
|
||||
func (t *TimeSync) doTimeSync() {
|
||||
metricTimeSyncStatus.Set(0)
|
||||
for {
|
||||
|
@ -154,16 +160,61 @@ func (t *TimeSync) Sync() error {
|
|||
offset *time.Duration
|
||||
)
|
||||
|
||||
syncMode := t.getSyncMode()
|
||||
|
||||
metricTimeSyncCount.Inc()
|
||||
|
||||
if syncMode.Ntp {
|
||||
now, offset = t.queryNetworkTime()
|
||||
}
|
||||
syncMode := t.getSyncMode()
|
||||
|
||||
if syncMode.Http && now == nil {
|
||||
now = t.queryAllHttpTime()
|
||||
Orders:
|
||||
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 {
|
||||
|
|
|
@ -19,6 +19,14 @@ func networkStateChanged() {
|
|||
// do not block the main thread
|
||||
go waitCtrlAndRequestDisplayUpdate(true)
|
||||
|
||||
if timeSync != nil {
|
||||
if networkState != nil {
|
||||
timeSync.SetDhcpNtpAddresses(networkState.NtpAddressesString())
|
||||
}
|
||||
|
||||
timeSync.Sync()
|
||||
}
|
||||
|
||||
// always restart mDNS when the network state changes
|
||||
if mDNS != nil {
|
||||
_ = mDNS.SetListenOptions(config.NetworkConfig.GetMDNSMode())
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -19,21 +19,21 @@
|
|||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^2.2.3",
|
||||
"@headlessui/react": "^2.2.4",
|
||||
"@headlessui/tailwindcss": "^0.2.2",
|
||||
"@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-fit": "^0.10.0",
|
||||
"@xterm/addon-unicode11": "^0.8.0",
|
||||
"@xterm/addon-web-links": "^0.11.0",
|
||||
"@xterm/addon-webgl": "^0.18.0",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"cva": "^1.0.0-beta.3",
|
||||
"cva": "^1.0.0-beta.4",
|
||||
"dayjs": "^1.11.13",
|
||||
"eslint-import-resolver-alias": "^1.1.2",
|
||||
"focus-trap-react": "^11.0.3",
|
||||
"framer-motion": "^12.11.4",
|
||||
"focus-trap-react": "^11.0.4",
|
||||
"framer-motion": "^12.23.0",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"mini-svg-data-uri": "^1.4.4",
|
||||
"react": "^19.1.0",
|
||||
|
@ -42,42 +42,42 @@
|
|||
"react-hot-toast": "^2.5.2",
|
||||
"react-icons": "^5.5.0",
|
||||
"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-xtermjs": "^1.0.10",
|
||||
"recharts": "^2.15.3",
|
||||
"tailwind-merge": "^3.3.0",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"usehooks-ts": "^3.1.1",
|
||||
"validator": "^13.15.0",
|
||||
"validator": "^13.15.15",
|
||||
"zustand": "^4.5.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/compat": "^1.2.9",
|
||||
"@eslint/compat": "^1.3.1",
|
||||
"@eslint/eslintrc": "^3.3.1",
|
||||
"@eslint/js": "^9.26.0",
|
||||
"@eslint/js": "^9.30.1",
|
||||
"@tailwindcss/forms": "^0.5.10",
|
||||
"@tailwindcss/postcss": "^4.1.7",
|
||||
"@tailwindcss/postcss": "^4.1.11",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@tailwindcss/vite": "^4.1.7",
|
||||
"@types/react": "^19.1.4",
|
||||
"@types/react-dom": "^19.1.5",
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"@types/semver": "^7.7.0",
|
||||
"@types/validator": "^13.15.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.32.1",
|
||||
"@typescript-eslint/parser": "^8.32.1",
|
||||
"@vitejs/plugin-react-swc": "^3.9.0",
|
||||
"@types/validator": "^13.15.2",
|
||||
"@typescript-eslint/eslint-plugin": "^8.35.1",
|
||||
"@typescript-eslint/parser": "^8.35.1",
|
||||
"@vitejs/plugin-react-swc": "^3.10.2",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"eslint": "^9.26.0",
|
||||
"eslint": "^9.30.1",
|
||||
"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-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.20",
|
||||
"globals": "^16.1.0",
|
||||
"postcss": "^8.5.3",
|
||||
"prettier": "^3.5.3",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
"tailwindcss": "^4.1.7",
|
||||
"globals": "^16.3.0",
|
||||
"postcss": "^8.5.6",
|
||||
"prettier": "^3.6.2",
|
||||
"prettier-plugin-tailwindcss": "^0.6.13",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "^6.3.5",
|
||||
"vite-tsconfig-paths": "^5.1.4"
|
||||
|
|
|
@ -67,19 +67,19 @@ function Terminal({
|
|||
}) {
|
||||
const enableTerminal = useUiStore(state => state.terminalType == type);
|
||||
const setTerminalType = useUiStore(state => state.setTerminalType);
|
||||
const setDisableKeyboardFocusTrap = useUiStore(state => state.setDisableVideoFocusTrap);
|
||||
const setDisableVideoFocusTrap = useUiStore(state => state.setDisableVideoFocusTrap);
|
||||
|
||||
const { instance, ref } = useXTerm({ options: TERMINAL_CONFIG });
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
setDisableKeyboardFocusTrap(enableTerminal);
|
||||
setDisableVideoFocusTrap(enableTerminal);
|
||||
}, 500);
|
||||
|
||||
return () => {
|
||||
setDisableKeyboardFocusTrap(false);
|
||||
setDisableVideoFocusTrap(false);
|
||||
};
|
||||
}, [ref, instance, enableTerminal, setDisableKeyboardFocusTrap, type]);
|
||||
}, [enableTerminal, setDisableVideoFocusTrap]);
|
||||
|
||||
const readyState = dataChannel.readyState;
|
||||
useEffect(() => {
|
||||
|
@ -116,7 +116,7 @@ function Terminal({
|
|||
const { domEvent } = e;
|
||||
if (domEvent.key === "Escape") {
|
||||
setTerminalType("none");
|
||||
setDisableKeyboardFocusTrap(false);
|
||||
setDisableVideoFocusTrap(false);
|
||||
domEvent.preventDefault();
|
||||
}
|
||||
});
|
||||
|
@ -131,7 +131,7 @@ function Terminal({
|
|||
onDataHandler.dispose();
|
||||
onKeyHandler.dispose();
|
||||
};
|
||||
}, [instance, dataChannel, readyState, setDisableKeyboardFocusTrap, setTerminalType]);
|
||||
}, [dataChannel, instance, readyState, setDisableVideoFocusTrap, setTerminalType]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!instance) return;
|
||||
|
@ -158,7 +158,7 @@ function Terminal({
|
|||
return () => {
|
||||
window.removeEventListener("resize", handleResize);
|
||||
};
|
||||
}, [ref, instance]);
|
||||
}, [instance]);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
|
@ -657,6 +657,16 @@ export default function WebRTCVideo() {
|
|||
return true;
|
||||
}, [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 (
|
||||
<div className="grid h-full w-full grid-rows-(--grid-layout)">
|
||||
<div className="flex min-h-[39.5px] flex-col">
|
||||
|
@ -691,17 +701,15 @@ export default function WebRTCVideo() {
|
|||
<div className="relative flex h-full w-full items-center justify-center">
|
||||
<video
|
||||
ref={videoElm}
|
||||
autoPlay={true}
|
||||
autoPlay
|
||||
controls={false}
|
||||
onPlaying={onVideoPlaying}
|
||||
onPlay={onVideoPlaying}
|
||||
muted={true}
|
||||
muted
|
||||
playsInline
|
||||
disablePictureInPicture
|
||||
controlsList="nofullscreen"
|
||||
style={{
|
||||
filter: `saturate(${videoSaturation}) brightness(${videoBrightness}) contrast(${videoContrast})`,
|
||||
}}
|
||||
style={videoStyle}
|
||||
className={cx(
|
||||
"max-h-full min-h-[384px] max-w-full min-w-[512px] bg-black/50 object-contain transition-all duration-1000",
|
||||
{
|
||||
|
|
|
@ -10,11 +10,11 @@ import { SettingsPageHeader } from "@components/SettingsPageheader";
|
|||
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import { useHidStore, useRTCStore, useUiStore, useSettingsStore } from "@/hooks/stores";
|
||||
import { keys, modifiers } from "@/keyboardMappings";
|
||||
import { layouts, chars } from "@/keyboardLayouts";
|
||||
import { KeyStroke, KeyboardLayout, selectedKeyboard } from "@/keyboardLayouts";
|
||||
import notifications from "@/notifications";
|
||||
|
||||
const hidKeyboardPayload = (keys: number[], modifier: number) => {
|
||||
return { keys, modifier };
|
||||
const hidKeyboardPayload = (modifier: number, keys: number[]) => {
|
||||
return { modifier, keys };
|
||||
};
|
||||
|
||||
const modifierCode = (shift?: boolean, altRight?: boolean) => {
|
||||
|
@ -62,49 +62,56 @@ export default function PasteModal() {
|
|||
const onConfirmPaste = useCallback(async () => {
|
||||
setPasteMode(false);
|
||||
setDisableVideoFocusTrap(false);
|
||||
|
||||
if (rpcDataChannel?.readyState !== "open" || !TextAreaRef.current) return;
|
||||
if (!safeKeyboardLayout) return;
|
||||
if (!chars[safeKeyboardLayout]) return;
|
||||
const keyboard: KeyboardLayout = selectedKeyboard(safeKeyboardLayout);
|
||||
if (!keyboard) return;
|
||||
|
||||
const text = TextAreaRef.current.value;
|
||||
|
||||
try {
|
||||
for (const char of text) {
|
||||
const { key, shift, altRight, deadKey, accentKey } = chars[safeKeyboardLayout][char]
|
||||
const keyprops = keyboard.chars[char];
|
||||
if (!keyprops) continue;
|
||||
|
||||
const { key, shift, altRight, deadKey, accentKey } = keyprops;
|
||||
if (!key) continue;
|
||||
|
||||
const keyz = [ keys[key] ];
|
||||
const modz = [ modifierCode(shift, altRight) ];
|
||||
|
||||
if (deadKey) {
|
||||
keyz.push(keys["Space"]);
|
||||
modz.push(noModifier);
|
||||
}
|
||||
// if this is an accented character, we need to send that accent FIRST
|
||||
if (accentKey) {
|
||||
keyz.unshift(keys[accentKey.key])
|
||||
modz.unshift(modifierCode(accentKey.shift, accentKey.altRight))
|
||||
await sendKeystroke({modifier: modifierCode(accentKey.shift, accentKey.altRight), keys: [ keys[accentKey.key] ] })
|
||||
}
|
||||
|
||||
for (const [index, kei] of keyz.entries()) {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
send(
|
||||
"keyboardReport",
|
||||
hidKeyboardPayload([kei], modz[index]),
|
||||
params => {
|
||||
if ("error" in params) return reject(params.error);
|
||||
send("keyboardReport", hidKeyboardPayload([], 0), params => {
|
||||
if ("error" in params) return reject(params.error);
|
||||
resolve();
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
// now send the actual key
|
||||
await sendKeystroke({ modifier: modifierCode(shift, altRight), keys: [ keys[key] ]});
|
||||
|
||||
// if what was requested was a dead key, we need to send an unmodified space to emit
|
||||
// just the accent character
|
||||
if (deadKey) {
|
||||
await sendKeystroke({ modifier: noModifier, keys: [ keys["Space"] ] });
|
||||
}
|
||||
|
||||
// now send a message with no keys down to "release" the keys
|
||||
await sendKeystroke({ modifier: 0, keys: [] });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
console.error("Failed to paste text:", error);
|
||||
notifications.error("Failed to paste text");
|
||||
}
|
||||
}, [rpcDataChannel?.readyState, send, setDisableVideoFocusTrap, setPasteMode, safeKeyboardLayout]);
|
||||
|
||||
async function sendKeystroke(stroke: KeyStroke) {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
send(
|
||||
"keyboardReport",
|
||||
hidKeyboardPayload(stroke.modifier, stroke.keys),
|
||||
params => {
|
||||
if ("error" in params) return reject(params.error);
|
||||
resolve();
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
}, [rpcDataChannel?.readyState, safeKeyboardLayout, send, setDisableVideoFocusTrap, setPasteMode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (TextAreaRef.current) {
|
||||
|
@ -154,7 +161,7 @@ export default function PasteModal() {
|
|||
// @ts-expect-error TS doesn't recognize Intl.Segmenter in some environments
|
||||
[...new Intl.Segmenter().segment(value)]
|
||||
.map(x => x.segment)
|
||||
.filter(char => !chars[safeKeyboardLayout][char]),
|
||||
.filter(char => !selectedKeyboard(safeKeyboardLayout).chars[char]),
|
||||
),
|
||||
];
|
||||
|
||||
|
@ -175,7 +182,7 @@ export default function PasteModal() {
|
|||
</div>
|
||||
<div className="space-y-4">
|
||||
<p className="text-xs text-slate-600 dark:text-slate-400">
|
||||
Sending text using keyboard layout: {layouts[safeKeyboardLayout]}
|
||||
Sending text using keyboard layout: {selectedKeyboard(safeKeyboardLayout).name}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -14,7 +14,7 @@ import AddDeviceForm from "./AddDeviceForm";
|
|||
export default function WakeOnLanModal() {
|
||||
const [storedDevices, setStoredDevices] = useState<StoredDevice[]>([]);
|
||||
const [showAddForm, setShowAddForm] = useState(false);
|
||||
const setDisableFocusTrap = useUiStore(state => state.setDisableVideoFocusTrap);
|
||||
const setDisableVideoFocusTrap = useUiStore(state => state.setDisableVideoFocusTrap);
|
||||
|
||||
const rpcDataChannel = useRTCStore(state => state.rpcDataChannel);
|
||||
|
||||
|
@ -24,9 +24,9 @@ export default function WakeOnLanModal() {
|
|||
const [addDeviceErrorMessage, setAddDeviceErrorMessage] = useState<string | null>(null);
|
||||
|
||||
const onCancelWakeOnLanModal = useCallback(() => {
|
||||
setDisableVideoFocusTrap(false);
|
||||
close();
|
||||
setDisableFocusTrap(false);
|
||||
}, [close, setDisableFocusTrap]);
|
||||
}, [close, setDisableVideoFocusTrap]);
|
||||
|
||||
const onSendMagicPacket = useCallback(
|
||||
(macAddress: string) => {
|
||||
|
@ -43,12 +43,12 @@ export default function WakeOnLanModal() {
|
|||
}
|
||||
} else {
|
||||
notifications.success("Magic Packet sent successfully");
|
||||
setDisableFocusTrap(false);
|
||||
setDisableVideoFocusTrap(false);
|
||||
close();
|
||||
}
|
||||
});
|
||||
},
|
||||
[close, rpcDataChannel?.readyState, send, setDisableFocusTrap],
|
||||
[close, rpcDataChannel?.readyState, send, setDisableVideoFocusTrap],
|
||||
);
|
||||
|
||||
const syncStoredDevices = useCallback(() => {
|
||||
|
@ -78,7 +78,7 @@ export default function WakeOnLanModal() {
|
|||
}
|
||||
});
|
||||
},
|
||||
[storedDevices, send, syncStoredDevices],
|
||||
[send, storedDevices, syncStoredDevices],
|
||||
);
|
||||
|
||||
const onAddDevice = useCallback(
|
||||
|
|
|
@ -935,5 +935,5 @@ export const useMacrosStore = create<MacrosState>((set, get) => ({
|
|||
} finally {
|
||||
set({ loading: false });
|
||||
}
|
||||
},
|
||||
}
|
||||
}));
|
||||
|
|
|
@ -1,45 +1,32 @@
|
|||
import { chars as chars_fr_BE, name as name_fr_BE } from "@/keyboardLayouts/fr_BE"
|
||||
import { chars as chars_cs_CZ, name as name_cs_CZ } from "@/keyboardLayouts/cs_CZ"
|
||||
import { chars as chars_en_UK, name as name_en_UK } from "@/keyboardLayouts/en_UK"
|
||||
import { chars as chars_en_US, name as name_en_US } from "@/keyboardLayouts/en_US"
|
||||
import { chars as chars_fr_FR, name as name_fr_FR } from "@/keyboardLayouts/fr_FR"
|
||||
import { chars as chars_de_DE, name as name_de_DE } from "@/keyboardLayouts/de_DE"
|
||||
import { chars as chars_it_IT, name as name_it_IT } from "@/keyboardLayouts/it_IT"
|
||||
import { chars as chars_nb_NO, name as name_nb_NO } from "@/keyboardLayouts/nb_NO"
|
||||
import { chars as chars_es_ES, name as name_es_ES } from "@/keyboardLayouts/es_ES"
|
||||
import { chars as chars_sv_SE, name as name_sv_SE } from "@/keyboardLayouts/sv_SE"
|
||||
import { chars as chars_fr_CH, name as name_fr_CH } from "@/keyboardLayouts/fr_CH"
|
||||
import { chars as chars_de_CH, name as name_de_CH } from "@/keyboardLayouts/de_CH"
|
||||
export interface KeyStroke { modifier: number; keys: number[]; }
|
||||
export interface KeyInfo { key: string | number; shift?: boolean, altRight?: boolean }
|
||||
export interface KeyCombo extends KeyInfo { deadKey?: boolean, accentKey?: KeyInfo }
|
||||
export interface KeyboardLayout { isoCode: string, name: string, chars: Record<string, KeyCombo> }
|
||||
|
||||
interface KeyInfo { key: string | number; shift?: boolean, altRight?: boolean }
|
||||
export type KeyCombo = KeyInfo & { deadKey?: boolean, accentKey?: KeyInfo }
|
||||
// to add a new layout, create a file like the above and add it to the list
|
||||
import { cs_CZ } from "@/keyboardLayouts/cs_CZ"
|
||||
import { de_CH } from "@/keyboardLayouts/de_CH"
|
||||
import { de_DE } from "@/keyboardLayouts/de_DE"
|
||||
import { en_US } from "@/keyboardLayouts/en_US"
|
||||
import { en_UK } from "@/keyboardLayouts/en_UK"
|
||||
import { es_ES } from "@/keyboardLayouts/es_ES"
|
||||
import { fr_BE } from "@/keyboardLayouts/fr_BE"
|
||||
import { fr_CH } from "@/keyboardLayouts/fr_CH"
|
||||
import { fr_FR } from "@/keyboardLayouts/fr_FR"
|
||||
import { it_IT } from "@/keyboardLayouts/it_IT"
|
||||
import { nb_NO } from "@/keyboardLayouts/nb_NO"
|
||||
import { sv_SE } from "@/keyboardLayouts/sv_SE"
|
||||
|
||||
export const layouts: Record<string, string> = {
|
||||
be_FR: name_fr_BE,
|
||||
cs_CZ: name_cs_CZ,
|
||||
en_UK: name_en_UK,
|
||||
en_US: name_en_US,
|
||||
fr_FR: name_fr_FR,
|
||||
de_DE: name_de_DE,
|
||||
it_IT: name_it_IT,
|
||||
nb_NO: name_nb_NO,
|
||||
es_ES: name_es_ES,
|
||||
sv_SE: name_sv_SE,
|
||||
fr_CH: name_fr_CH,
|
||||
de_CH: name_de_CH,
|
||||
}
|
||||
export const keyboards: KeyboardLayout[] = [ cs_CZ, de_CH, de_DE, en_UK, en_US, es_ES, fr_BE, fr_CH, fr_FR, it_IT, nb_NO, sv_SE ];
|
||||
|
||||
export const chars: Record<string, Record<string, KeyCombo>> = {
|
||||
be_FR: chars_fr_BE,
|
||||
cs_CZ: chars_cs_CZ,
|
||||
en_UK: chars_en_UK,
|
||||
en_US: chars_en_US,
|
||||
fr_FR: chars_fr_FR,
|
||||
de_DE: chars_de_DE,
|
||||
it_IT: chars_it_IT,
|
||||
nb_NO: chars_nb_NO,
|
||||
es_ES: chars_es_ES,
|
||||
sv_SE: chars_sv_SE,
|
||||
fr_CH: chars_fr_CH,
|
||||
de_CH: chars_de_CH,
|
||||
export const selectedKeyboard = (isoCode: string): KeyboardLayout => {
|
||||
// fallback to original behaviour of en-US if no isoCode given
|
||||
return keyboards.find(keyboard => keyboard.isoCode == isoCode)
|
||||
?? keyboards.find(keyboard => keyboard.isoCode == "en-US")!;
|
||||
};
|
||||
|
||||
export const keyboardOptions = () => {
|
||||
return keyboards.map((keyboard) => {
|
||||
return { label: keyboard.name, value: keyboard.isoCode }
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { KeyCombo } from "../keyboardLayouts"
|
||||
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
|
||||
|
||||
export const name = "Čeština";
|
||||
const name = "Čeština";
|
||||
|
||||
const keyTrema = { key: "Backslash" } // tréma (umlaut), two dots placed above a vowel
|
||||
const keyAcute = { key: "Equal" } // accent aigu (acute accent), mark ´ placed above the letter
|
||||
|
@ -13,7 +13,7 @@ const keyOverdot = { key: "Digit8", shift: true, altRight: true } // overdot (do
|
|||
const keyHook = { key: "Digit6", shift: true, altRight: true } // ogonoek (little hook), mark ˛ placed beneath a letter
|
||||
const keyCedille = { key: "Equal", shift: true, altRight: true } // accent cedille (cedilla), mark ¸ placed beneath a letter
|
||||
|
||||
export const chars = {
|
||||
const chars = {
|
||||
A: { key: "KeyA", shift: true },
|
||||
"Ä": { key: "KeyA", shift: true, accentKey: keyTrema },
|
||||
"Á": { key: "KeyA", shift: true, accentKey: keyAcute },
|
||||
|
@ -242,3 +242,9 @@ export const chars = {
|
|||
Enter: { key: "Enter" },
|
||||
Tab: { key: "Tab" },
|
||||
} as Record<string, KeyCombo>;
|
||||
|
||||
export const cs_CZ: KeyboardLayout = {
|
||||
isoCode: "cs-CZ",
|
||||
name: name,
|
||||
chars: chars
|
||||
};
|
|
@ -1,6 +1,6 @@
|
|||
import { KeyCombo } from "../keyboardLayouts"
|
||||
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
|
||||
|
||||
export const name = "Schwiizerdütsch";
|
||||
const name = "Schwiizerdütsch";
|
||||
|
||||
const keyTrema = { key: "BracketRight" } // tréma (umlaut), two dots placed above a vowel
|
||||
const keyAcute = { key: "Minus", altRight: true } // accent aigu (acute accent), mark ´ placed above the letter
|
||||
|
@ -8,7 +8,7 @@ const keyHat = { key: "Equal" } // accent circonflexe (accent hat), mark ^ place
|
|||
const keyGrave = { key: "Equal", shift: true } // accent grave, mark ` placed above the letter
|
||||
const keyTilde = { key: "Equal", altRight: true } // tilde, mark ~ placed above the letter
|
||||
|
||||
export const chars = {
|
||||
const chars = {
|
||||
A: { key: "KeyA", shift: true },
|
||||
"Ä": { key: "KeyA", shift: true, accentKey: keyTrema },
|
||||
"Á": { key: "KeyA", shift: true, accentKey: keyAcute },
|
||||
|
@ -163,3 +163,9 @@ export const chars = {
|
|||
Enter: { key: "Enter" },
|
||||
Tab: { key: "Tab" },
|
||||
} as Record<string, KeyCombo>;
|
||||
|
||||
export const de_CH: KeyboardLayout = {
|
||||
isoCode: "de-CH",
|
||||
name: name,
|
||||
chars: chars
|
||||
};
|
|
@ -1,12 +1,12 @@
|
|||
import { KeyCombo } from "../keyboardLayouts"
|
||||
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
|
||||
|
||||
export const name = "Deutsch";
|
||||
const name = "Deutsch";
|
||||
|
||||
const keyAcute = { key: "Equal" } // accent aigu (acute accent), mark ´ placed above the letter
|
||||
const keyHat = { key: "Backquote" } // accent circonflexe (accent hat), mark ^ placed above the letter
|
||||
const keyGrave = { key: "Equal", shift: true } // accent grave, mark ` placed above the letter
|
||||
|
||||
export const chars = {
|
||||
const chars = {
|
||||
A: { key: "KeyA", shift: true },
|
||||
"Á": { key: "KeyA", shift: true, accentKey: keyAcute },
|
||||
"Â": { key: "KeyA", shift: true, accentKey: keyHat },
|
||||
|
@ -150,3 +150,9 @@ export const chars = {
|
|||
Enter: { key: "Enter" },
|
||||
Tab: { key: "Tab" },
|
||||
} as Record<string, KeyCombo>;
|
||||
|
||||
export const de_DE: KeyboardLayout = {
|
||||
isoCode: "de-DE",
|
||||
name: name,
|
||||
chars: chars
|
||||
};
|
|
@ -1,8 +1,8 @@
|
|||
import { KeyCombo } from "../keyboardLayouts"
|
||||
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
|
||||
|
||||
export const name = "English (UK)";
|
||||
const name = "English (UK)";
|
||||
|
||||
export const chars = {
|
||||
const chars = {
|
||||
A: { key: "KeyA", shift: true },
|
||||
B: { key: "KeyB", shift: true },
|
||||
C: { key: "KeyC", shift: true },
|
||||
|
@ -105,3 +105,9 @@ export const chars = {
|
|||
Enter: { key: "Enter" },
|
||||
Tab: { key: "Tab" },
|
||||
} as Record<string, KeyCombo>
|
||||
|
||||
export const en_UK: KeyboardLayout = {
|
||||
isoCode: "en-UK",
|
||||
name: name,
|
||||
chars: chars
|
||||
};
|
|
@ -1,8 +1,8 @@
|
|||
import { KeyCombo } from "../keyboardLayouts"
|
||||
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
|
||||
|
||||
export const name = "English (US)";
|
||||
const name = "English (US)";
|
||||
|
||||
export const chars = {
|
||||
const chars = {
|
||||
A: { key: "KeyA", shift: true },
|
||||
B: { key: "KeyB", shift: true },
|
||||
C: { key: "KeyC", shift: true },
|
||||
|
@ -111,3 +111,9 @@ export const chars = {
|
|||
Insert: { key: "Insert", shift: false },
|
||||
Delete: { key: "Delete", shift: false },
|
||||
} as Record<string, KeyCombo>
|
||||
|
||||
export const en_US: KeyboardLayout = {
|
||||
isoCode: "en-US",
|
||||
name: name,
|
||||
chars: chars
|
||||
};
|
|
@ -1,6 +1,6 @@
|
|||
import { KeyCombo } from "../keyboardLayouts"
|
||||
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
|
||||
|
||||
export const name = "Español";
|
||||
const name = "Español";
|
||||
|
||||
const keyTrema = { key: "Quote", shift: true } // tréma (umlaut), two dots placed above a vowel
|
||||
const keyAcute = { key: "Quote" } // accent aigu (acute accent), mark ´ placed above the letter
|
||||
|
@ -8,7 +8,7 @@ const keyHat = { key: "BracketRight", shift: true } // accent circonflexe (accen
|
|||
const keyGrave = { key: "BracketRight" } // accent grave, mark ` placed above the letter
|
||||
const keyTilde = { key: "Key4", altRight: true } // tilde, mark ~ placed above the letter
|
||||
|
||||
export const chars = {
|
||||
const chars = {
|
||||
A: { key: "KeyA", shift: true },
|
||||
"Ä": { key: "KeyA", shift: true, accentKey: keyTrema },
|
||||
"Á": { key: "KeyA", shift: true, accentKey: keyAcute },
|
||||
|
@ -166,3 +166,9 @@ export const chars = {
|
|||
Enter: { key: "Enter" },
|
||||
Tab: { key: "Tab" },
|
||||
} as Record<string, KeyCombo>;
|
||||
|
||||
export const es_ES: KeyboardLayout = {
|
||||
isoCode: "es-ES",
|
||||
name: name,
|
||||
chars: chars
|
||||
};
|
|
@ -1,6 +1,6 @@
|
|||
import { KeyCombo } from "../keyboardLayouts"
|
||||
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
|
||||
|
||||
export const name = "Belgisch Nederlands";
|
||||
const name = "Belgisch Nederlands";
|
||||
|
||||
const keyTrema = { key: "BracketLeft", shift: true } // tréma (umlaut), two dots placed above a vowel
|
||||
const keyHat = { key: "BracketLeft" } // accent circonflexe (accent hat), mark ^ placed above the letter
|
||||
|
@ -8,7 +8,7 @@ const keyAcute = { key: "Semicolon", altRight: true } // accent aigu (acute acce
|
|||
const keyGrave = { key: "Quote", shift: true } // accent grave, mark ` placed above the letter
|
||||
const keyTilde = { key: "Slash", altRight: true } // tilde, mark ~ placed above the letter
|
||||
|
||||
export const chars = {
|
||||
const chars = {
|
||||
A: { key: "KeyQ", shift: true },
|
||||
"Ä": { key: "KeyQ", shift: true, accentKey: keyTrema },
|
||||
"Â": { key: "KeyQ", shift: true, accentKey: keyHat },
|
||||
|
@ -165,3 +165,9 @@ export const chars = {
|
|||
Enter: { key: "Enter" },
|
||||
Tab: { key: "Tab" },
|
||||
} as Record<string, KeyCombo>;
|
||||
|
||||
export const fr_BE: KeyboardLayout = {
|
||||
isoCode: "fr-BE",
|
||||
name: name,
|
||||
chars: chars
|
||||
};
|
|
@ -1,11 +1,11 @@
|
|||
import { KeyCombo } from "../keyboardLayouts"
|
||||
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
|
||||
|
||||
import { chars as chars_de_CH } from "./de_CH"
|
||||
import { de_CH } from "./de_CH"
|
||||
|
||||
export const name = "Français de Suisse";
|
||||
const name = "Français de Suisse";
|
||||
|
||||
export const chars = {
|
||||
...chars_de_CH,
|
||||
const chars = {
|
||||
...de_CH.chars,
|
||||
"è": { key: "BracketLeft" },
|
||||
"ü": { key: "BracketLeft", shift: true },
|
||||
"é": { key: "Semicolon" },
|
||||
|
@ -13,3 +13,9 @@ export const chars = {
|
|||
"à": { key: "Quote" },
|
||||
"ä": { key: "Quote", shift: true },
|
||||
} as Record<string, KeyCombo>;
|
||||
|
||||
export const fr_CH: KeyboardLayout = {
|
||||
isoCode: "fr-CH",
|
||||
name: name,
|
||||
chars: chars
|
||||
};
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { KeyCombo } from "../keyboardLayouts"
|
||||
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
|
||||
|
||||
export const name = "Français";
|
||||
const name = "Français";
|
||||
|
||||
const keyTrema = { key: "BracketLeft", shift: true } // tréma (umlaut), two dots placed above a vowel
|
||||
const keyHat = { key: "BracketLeft" } // accent circonflexe (accent hat), mark ^ placed above the letter
|
||||
|
||||
export const chars = {
|
||||
const chars = {
|
||||
A: { key: "KeyQ", shift: true },
|
||||
"Ä": { key: "KeyQ", shift: true, accentKey: keyTrema },
|
||||
"Â": { key: "KeyQ", shift: true, accentKey: keyHat },
|
||||
|
@ -137,3 +137,9 @@ export const chars = {
|
|||
Enter: { key: "Enter" },
|
||||
Tab: { key: "Tab" },
|
||||
} as Record<string, KeyCombo>;
|
||||
|
||||
export const fr_FR: KeyboardLayout = {
|
||||
isoCode: "fr-FR",
|
||||
name: name,
|
||||
chars: chars
|
||||
};
|
|
@ -1,8 +1,8 @@
|
|||
import { KeyCombo } from "../keyboardLayouts"
|
||||
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
|
||||
|
||||
export const name = "Italiano";
|
||||
const name = "Italiano";
|
||||
|
||||
export const chars = {
|
||||
const chars = {
|
||||
A: { key: "KeyA", shift: true },
|
||||
B: { key: "KeyB", shift: true },
|
||||
C: { key: "KeyC", shift: true },
|
||||
|
@ -111,3 +111,9 @@ export const chars = {
|
|||
Enter: { key: "Enter" },
|
||||
Tab: { key: "Tab" },
|
||||
} as Record<string, KeyCombo>;
|
||||
|
||||
export const it_IT: KeyboardLayout = {
|
||||
isoCode: "it-IT",
|
||||
name: name,
|
||||
chars: chars
|
||||
};
|
|
@ -1,6 +1,6 @@
|
|||
import { KeyCombo } from "../keyboardLayouts"
|
||||
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
|
||||
|
||||
export const name = "Norsk bokmål";
|
||||
const name = "Norsk bokmål";
|
||||
|
||||
const keyTrema = { key: "BracketRight" } // tréma (umlaut), two dots placed above a vowel
|
||||
const keyAcute = { key: "Equal", altRight: true } // accent aigu (acute accent), mark ´ placed above the letter
|
||||
|
@ -8,7 +8,7 @@ const keyHat = { key: "BracketRight", shift: true } // accent circonflexe (accen
|
|||
const keyGrave = { key: "Equal", shift: true } // accent grave, mark ` placed above the letter
|
||||
const keyTilde = { key: "BracketRight", altRight: true } // tilde, mark ~ placed above the letter
|
||||
|
||||
export const chars = {
|
||||
const chars = {
|
||||
A: { key: "KeyA", shift: true },
|
||||
"Ä": { key: "KeyA", shift: true, accentKey: keyTrema },
|
||||
"Á": { key: "KeyA", shift: true, accentKey: keyAcute },
|
||||
|
@ -165,3 +165,9 @@ export const chars = {
|
|||
Enter: { key: "Enter" },
|
||||
Tab: { key: "Tab" },
|
||||
} as Record<string, KeyCombo>;
|
||||
|
||||
export const nb_NO: KeyboardLayout = {
|
||||
isoCode: "nb-NO",
|
||||
name: name,
|
||||
chars: chars
|
||||
};
|
|
@ -1,6 +1,6 @@
|
|||
import { KeyCombo } from "../keyboardLayouts"
|
||||
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
|
||||
|
||||
export const name = "Svenska";
|
||||
const name = "Svenska";
|
||||
|
||||
const keyTrema = { key: "BracketRight" } // tréma (umlaut), two dots placed above a vowel
|
||||
const keyAcute = { key: "Equal" } // accent aigu (acute accent), mark ´ placed above the letter
|
||||
|
@ -8,7 +8,7 @@ const keyHat = { key: "BracketRight", shift: true } // accent circonflexe (accen
|
|||
const keyGrave = { key: "Equal", shift: true } // accent grave, mark ` placed above the letter
|
||||
const keyTilde = { key: "BracketRight", altRight: true } // tilde, mark ~ placed above the letter
|
||||
|
||||
export const chars = {
|
||||
const chars = {
|
||||
A: { key: "KeyA", shift: true },
|
||||
"Á": { key: "KeyA", shift: true, accentKey: keyAcute },
|
||||
"Â": { key: "KeyA", shift: true, accentKey: keyHat },
|
||||
|
@ -162,3 +162,9 @@ export const chars = {
|
|||
Enter: { key: "Enter" },
|
||||
Tab: { key: "Tab" },
|
||||
} as Record<string, KeyCombo>;
|
||||
|
||||
export const sv_SE: KeyboardLayout = {
|
||||
isoCode: "sv-SE",
|
||||
name: name,
|
||||
chars: chars
|
||||
};
|
|
@ -1,17 +1,19 @@
|
|||
// Key codes and modifiers correspond to definitions in the
|
||||
// [Linux USB HID gadget driver](https://www.kernel.org/doc/Documentation/usb/gadget_hid.txt)
|
||||
// [Section 10. Keyboard/Keypad Page 0x07](https://usb.org/sites/default/files/hut1_21.pdf)
|
||||
export const keys = {
|
||||
ArrowDown: 0x51,
|
||||
ArrowLeft: 0x50,
|
||||
ArrowRight: 0x4f,
|
||||
ArrowUp: 0x52,
|
||||
Backquote: 0x35,
|
||||
Backquote: 0x35, // aka Grave
|
||||
Backslash: 0x31,
|
||||
Backspace: 0x2a,
|
||||
BracketLeft: 0x2f,
|
||||
BracketRight: 0x30,
|
||||
BracketLeft: 0x2f, // aka LeftBrace
|
||||
BracketRight: 0x30, // aka RightBrace
|
||||
CapsLock: 0x39,
|
||||
Comma: 0x36,
|
||||
Compose: 0x65,
|
||||
ContextMenu: 0,
|
||||
Delete: 0x4c,
|
||||
Digit0: 0x27,
|
||||
|
@ -40,10 +42,21 @@ export const keys = {
|
|||
F10: 0x43,
|
||||
F11: 0x44,
|
||||
F12: 0x45,
|
||||
F13: 0x68,
|
||||
F14: 0x69,
|
||||
F15: 0x6a,
|
||||
F16: 0x6b,
|
||||
F17: 0x6c,
|
||||
F18: 0x6d,
|
||||
F19: 0x6e,
|
||||
F20: 0x6f,
|
||||
F21: 0x70,
|
||||
F22: 0x71,
|
||||
F23: 0x72,
|
||||
F24: 0x73,
|
||||
Home: 0x4a,
|
||||
HashTilde: 0x32, // non-US # and ~
|
||||
Insert: 0x49,
|
||||
IntlBackslash: 0x64,
|
||||
IntlBackslash: 0x64, // non-US \ and |
|
||||
KeyA: 0x04,
|
||||
KeyB: 0x05,
|
||||
KeyC: 0x06,
|
||||
|
@ -72,30 +85,35 @@ export const keys = {
|
|||
KeyZ: 0x1d,
|
||||
KeypadExclamation: 0xcf,
|
||||
Minus: 0x2d,
|
||||
NumLock: 0x53,
|
||||
Numpad0: 0x62,
|
||||
Numpad1: 0x59,
|
||||
Numpad2: 0x5a,
|
||||
Numpad3: 0x5b,
|
||||
Numpad4: 0x5c,
|
||||
None: 0x00,
|
||||
NumLock: 0x53, // and Clear
|
||||
Numpad0: 0x62, // and Insert
|
||||
Numpad1: 0x59, // and End
|
||||
Numpad2: 0x5a, // and Down Arrow
|
||||
Numpad3: 0x5b, // and Page Down
|
||||
Numpad4: 0x5c, // and Left Arrow
|
||||
Numpad5: 0x5d,
|
||||
Numpad6: 0x5e,
|
||||
Numpad7: 0x5f,
|
||||
Numpad8: 0x60,
|
||||
Numpad9: 0x61,
|
||||
Numpad6: 0x5e, // and Right Arrow
|
||||
Numpad7: 0x5f, // and Home
|
||||
Numpad8: 0x60, // and Up Arrow
|
||||
Numpad9: 0x61, // and Page Up
|
||||
NumpadAdd: 0x57,
|
||||
NumpadComma: 0x85,
|
||||
NumpadDecimal: 0x63,
|
||||
NumpadDivide: 0x54,
|
||||
NumpadEnter: 0x58,
|
||||
NumpadEqual: 0x67,
|
||||
NumpadLeftParen: 0xb6,
|
||||
NumpadMultiply: 0x55,
|
||||
NumpadRightParen: 0xb7,
|
||||
NumpadSubtract: 0x56,
|
||||
NumpadDecimal: 0x63,
|
||||
PageDown: 0x4e,
|
||||
PageUp: 0x4b,
|
||||
Period: 0x37,
|
||||
PrintScreen: 0x46,
|
||||
Pause: 0x48,
|
||||
Quote: 0x34,
|
||||
Power: 0x66,
|
||||
Quote: 0x34, // aka Single Quote or Apostrophe
|
||||
ScrollLock: 0x47,
|
||||
Semicolon: 0x33,
|
||||
Slash: 0x38,
|
||||
|
|
|
@ -4,7 +4,7 @@ import { KeyboardLedSync, useSettingsStore } from "@/hooks/stores";
|
|||
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import notifications from "@/notifications";
|
||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||
import { layouts } from "@/keyboardLayouts";
|
||||
import { keyboardOptions } from "@/keyboardLayouts";
|
||||
import { Checkbox } from "@/components/Checkbox";
|
||||
|
||||
import { SelectMenuBasic } from "../components/SelectMenuBasic";
|
||||
|
@ -32,7 +32,7 @@ export default function SettingsKeyboardRoute() {
|
|||
return "en_US";
|
||||
}, [keyboardLayout]);
|
||||
|
||||
const layoutOptions = Object.entries(layouts).map(([code, language]) => { return { value: code, label: language } })
|
||||
const layoutOptions = keyboardOptions();
|
||||
const ledSyncOptions = [
|
||||
{ value: "auto", label: "Automatic" },
|
||||
{ value: "browser", label: "Browser Only" },
|
||||
|
|
|
@ -79,7 +79,7 @@ export default function SettingsRoute() {
|
|||
return () => {
|
||||
setDisableVideoFocusTrap(false);
|
||||
};
|
||||
}, [setDisableVideoFocusTrap, sendKeyboardEvent]);
|
||||
}, [sendKeyboardEvent, setDisableVideoFocusTrap]);
|
||||
|
||||
return (
|
||||
<div className="pointer-events-auto relative mx-auto max-w-4xl translate-x-0 transform text-left dark:text-white">
|
||||
|
|
|
@ -707,7 +707,7 @@ export default function KvmIdRoute() {
|
|||
}, [diskChannel, file]);
|
||||
|
||||
// System update
|
||||
const disableKeyboardFocusTrap = useUiStore(state => state.disableVideoFocusTrap);
|
||||
const disableVideoFocusTrap = useUiStore(state => state.disableVideoFocusTrap);
|
||||
|
||||
const [kvmTerminal, setKvmTerminal] = useState<RTCDataChannel | null>(null);
|
||||
const [serialConsole, setSerialConsole] = useState<RTCDataChannel | null>(null);
|
||||
|
@ -805,7 +805,7 @@ export default function KvmIdRoute() {
|
|||
)}
|
||||
<div className="relative h-full">
|
||||
<FocusTrap
|
||||
paused={disableKeyboardFocusTrap}
|
||||
paused={disableVideoFocusTrap}
|
||||
focusTrapOptions={{
|
||||
allowOutsideClick: true,
|
||||
escapeDeactivates: false,
|
||||
|
|
Loading…
Reference in New Issue