mirror of https://github.com/jetkvm/kvm.git
Merge cbc3f2016f
into bb87fb5a1a
This commit is contained in:
commit
8f86d6ed26
|
@ -114,7 +114,7 @@ var defaultConfig = &Config{
|
||||||
ActiveExtension: "",
|
ActiveExtension: "",
|
||||||
KeyboardMacros: []KeyboardMacro{},
|
KeyboardMacros: []KeyboardMacro{},
|
||||||
DisplayRotation: "270",
|
DisplayRotation: "270",
|
||||||
KeyboardLayout: "en_US",
|
KeyboardLayout: "en-US",
|
||||||
DisplayMaxBrightness: 64,
|
DisplayMaxBrightness: 64,
|
||||||
DisplayDimAfterSec: 120, // 2 minutes
|
DisplayDimAfterSec: 120, // 2 minutes
|
||||||
DisplayOffAfterSec: 1800, // 30 minutes
|
DisplayOffAfterSec: 1800, // 30 minutes
|
||||||
|
|
20
display.go
20
display.go
|
@ -30,7 +30,7 @@ const (
|
||||||
// do not call this function directly, use switchToScreenIfDifferent instead
|
// do not call this function directly, use switchToScreenIfDifferent instead
|
||||||
// this function is not thread safe
|
// this function is not thread safe
|
||||||
func switchToScreen(screen string) {
|
func switchToScreen(screen string) {
|
||||||
_, err := CallCtrlAction("lv_scr_load", map[string]interface{}{"obj": screen})
|
_, err := CallCtrlAction("lv_scr_load", map[string]any{"obj": screen})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
displayLogger.Warn().Err(err).Str("screen", screen).Msg("failed to switch to screen")
|
displayLogger.Warn().Err(err).Str("screen", screen).Msg("failed to switch to screen")
|
||||||
return
|
return
|
||||||
|
@ -39,15 +39,15 @@ func switchToScreen(screen string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func lvObjSetState(objName string, state string) (*CtrlResponse, error) {
|
func lvObjSetState(objName string, state string) (*CtrlResponse, error) {
|
||||||
return CallCtrlAction("lv_obj_set_state", map[string]interface{}{"obj": objName, "state": state})
|
return CallCtrlAction("lv_obj_set_state", map[string]any{"obj": objName, "state": state})
|
||||||
}
|
}
|
||||||
|
|
||||||
func lvObjAddFlag(objName string, flag string) (*CtrlResponse, error) {
|
func lvObjAddFlag(objName string, flag string) (*CtrlResponse, error) {
|
||||||
return CallCtrlAction("lv_obj_add_flag", map[string]interface{}{"obj": objName, "flag": flag})
|
return CallCtrlAction("lv_obj_add_flag", map[string]any{"obj": objName, "flag": flag})
|
||||||
}
|
}
|
||||||
|
|
||||||
func lvObjClearFlag(objName string, flag string) (*CtrlResponse, error) {
|
func lvObjClearFlag(objName string, flag string) (*CtrlResponse, error) {
|
||||||
return CallCtrlAction("lv_obj_clear_flag", map[string]interface{}{"obj": objName, "flag": flag})
|
return CallCtrlAction("lv_obj_clear_flag", map[string]any{"obj": objName, "flag": flag})
|
||||||
}
|
}
|
||||||
|
|
||||||
func lvObjHide(objName string) (*CtrlResponse, error) {
|
func lvObjHide(objName string) (*CtrlResponse, error) {
|
||||||
|
@ -59,27 +59,27 @@ func lvObjShow(objName string) (*CtrlResponse, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func lvObjSetOpacity(objName string, opacity int) (*CtrlResponse, error) { // nolint:unused
|
func lvObjSetOpacity(objName string, opacity int) (*CtrlResponse, error) { // nolint:unused
|
||||||
return CallCtrlAction("lv_obj_set_style_opa_layered", map[string]interface{}{"obj": objName, "opa": opacity})
|
return CallCtrlAction("lv_obj_set_style_opa_layered", map[string]any{"obj": objName, "opa": opacity})
|
||||||
}
|
}
|
||||||
|
|
||||||
func lvObjFadeIn(objName string, duration uint32) (*CtrlResponse, error) {
|
func lvObjFadeIn(objName string, duration uint32) (*CtrlResponse, error) {
|
||||||
return CallCtrlAction("lv_obj_fade_in", map[string]interface{}{"obj": objName, "time": duration})
|
return CallCtrlAction("lv_obj_fade_in", map[string]any{"obj": objName, "time": duration})
|
||||||
}
|
}
|
||||||
|
|
||||||
func lvObjFadeOut(objName string, duration uint32) (*CtrlResponse, error) {
|
func lvObjFadeOut(objName string, duration uint32) (*CtrlResponse, error) {
|
||||||
return CallCtrlAction("lv_obj_fade_out", map[string]interface{}{"obj": objName, "time": duration})
|
return CallCtrlAction("lv_obj_fade_out", map[string]any{"obj": objName, "time": duration})
|
||||||
}
|
}
|
||||||
|
|
||||||
func lvLabelSetText(objName string, text string) (*CtrlResponse, error) {
|
func lvLabelSetText(objName string, text string) (*CtrlResponse, error) {
|
||||||
return CallCtrlAction("lv_label_set_text", map[string]interface{}{"obj": objName, "text": text})
|
return CallCtrlAction("lv_label_set_text", map[string]any{"obj": objName, "text": text})
|
||||||
}
|
}
|
||||||
|
|
||||||
func lvImgSetSrc(objName string, src string) (*CtrlResponse, error) {
|
func lvImgSetSrc(objName string, src string) (*CtrlResponse, error) {
|
||||||
return CallCtrlAction("lv_img_set_src", map[string]interface{}{"obj": objName, "src": src})
|
return CallCtrlAction("lv_img_set_src", map[string]any{"obj": objName, "src": src})
|
||||||
}
|
}
|
||||||
|
|
||||||
func lvDispSetRotation(rotation string) (*CtrlResponse, error) {
|
func lvDispSetRotation(rotation string) (*CtrlResponse, error) {
|
||||||
return CallCtrlAction("lv_disp_set_rotation", map[string]interface{}{"rotation": rotation})
|
return CallCtrlAction("lv_disp_set_rotation", map[string]any{"rotation": rotation})
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateLabelIfChanged(objName string, newText string) {
|
func updateLabelIfChanged(objName string, newText string) {
|
||||||
|
|
|
@ -16,22 +16,22 @@ import (
|
||||||
type FieldConfig struct {
|
type FieldConfig struct {
|
||||||
Name string
|
Name string
|
||||||
Required bool
|
Required bool
|
||||||
RequiredIf map[string]interface{}
|
RequiredIf map[string]any
|
||||||
OneOf []string
|
OneOf []string
|
||||||
ValidateTypes []string
|
ValidateTypes []string
|
||||||
Defaults interface{}
|
Defaults any
|
||||||
IsEmpty bool
|
IsEmpty bool
|
||||||
CurrentValue interface{}
|
CurrentValue any
|
||||||
TypeString string
|
TypeString string
|
||||||
Delegated bool
|
Delegated bool
|
||||||
shouldUpdateValue bool
|
shouldUpdateValue bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetDefaultsAndValidate(config interface{}) error {
|
func SetDefaultsAndValidate(config any) error {
|
||||||
return setDefaultsAndValidate(config, true)
|
return setDefaultsAndValidate(config, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func setDefaultsAndValidate(config interface{}, isRoot bool) error {
|
func setDefaultsAndValidate(config any, isRoot bool) error {
|
||||||
// first we need to check if the config is a pointer
|
// first we need to check if the config is a pointer
|
||||||
if reflect.TypeOf(config).Kind() != reflect.Ptr {
|
if reflect.TypeOf(config).Kind() != reflect.Ptr {
|
||||||
return fmt.Errorf("config is not a pointer")
|
return fmt.Errorf("config is not a pointer")
|
||||||
|
@ -55,7 +55,7 @@ func setDefaultsAndValidate(config interface{}, isRoot bool) error {
|
||||||
Name: field.Name,
|
Name: field.Name,
|
||||||
OneOf: splitString(field.Tag.Get("one_of")),
|
OneOf: splitString(field.Tag.Get("one_of")),
|
||||||
ValidateTypes: splitString(field.Tag.Get("validate_type")),
|
ValidateTypes: splitString(field.Tag.Get("validate_type")),
|
||||||
RequiredIf: make(map[string]interface{}),
|
RequiredIf: make(map[string]any),
|
||||||
CurrentValue: fieldValue.Interface(),
|
CurrentValue: fieldValue.Interface(),
|
||||||
IsEmpty: false,
|
IsEmpty: false,
|
||||||
TypeString: fieldType,
|
TypeString: fieldType,
|
||||||
|
@ -142,8 +142,8 @@ func setDefaultsAndValidate(config interface{}, isRoot bool) error {
|
||||||
// now check if the field has required_if
|
// now check if the field has required_if
|
||||||
requiredIf := field.Tag.Get("required_if")
|
requiredIf := field.Tag.Get("required_if")
|
||||||
if requiredIf != "" {
|
if requiredIf != "" {
|
||||||
requiredIfParts := strings.Split(requiredIf, ",")
|
requiredIfParts := strings.SplitSeq(requiredIf, ",")
|
||||||
for _, part := range requiredIfParts {
|
for part := range requiredIfParts {
|
||||||
partVal := strings.SplitN(part, "=", 2)
|
partVal := strings.SplitN(part, "=", 2)
|
||||||
if len(partVal) != 2 {
|
if len(partVal) != 2 {
|
||||||
return fmt.Errorf("invalid required_if for field `%s`: %s", field.Name, requiredIf)
|
return fmt.Errorf("invalid required_if for field `%s`: %s", field.Name, requiredIf)
|
||||||
|
@ -168,7 +168,7 @@ func setDefaultsAndValidate(config interface{}, isRoot bool) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateFields(config interface{}, fields map[string]FieldConfig) error {
|
func validateFields(config any, fields map[string]FieldConfig) error {
|
||||||
// now we can start to validate the fields
|
// now we can start to validate the fields
|
||||||
for _, fieldConfig := range fields {
|
for _, fieldConfig := range fields {
|
||||||
if err := fieldConfig.validate(fields); err != nil {
|
if err := fieldConfig.validate(fields); err != nil {
|
||||||
|
@ -215,7 +215,7 @@ func (f *FieldConfig) validate(fields map[string]FieldConfig) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *FieldConfig) populate(config interface{}) {
|
func (f *FieldConfig) populate(config any) {
|
||||||
// update the field if it's not empty
|
// update the field if it's not empty
|
||||||
if !f.shouldUpdateValue {
|
if !f.shouldUpdateValue {
|
||||||
return
|
return
|
||||||
|
|
|
@ -16,7 +16,7 @@ func splitString(s string) []string {
|
||||||
return strings.Split(s, ",")
|
return strings.Split(s, ",")
|
||||||
}
|
}
|
||||||
|
|
||||||
func toString(v interface{}) (string, error) {
|
func toString(v any) (string, error) {
|
||||||
switch v := v.(type) {
|
switch v := v.(type) {
|
||||||
case string:
|
case string:
|
||||||
return v, nil
|
return v, nil
|
||||||
|
|
|
@ -50,7 +50,7 @@ var (
|
||||||
TimeFormat: time.RFC3339,
|
TimeFormat: time.RFC3339,
|
||||||
PartsOrder: []string{"time", "level", "scope", "component", "message"},
|
PartsOrder: []string{"time", "level", "scope", "component", "message"},
|
||||||
FieldsExclude: []string{"scope", "component"},
|
FieldsExclude: []string{"scope", "component"},
|
||||||
FormatPartValueByName: func(value interface{}, name string) string {
|
FormatPartValueByName: func(value any, name string) string {
|
||||||
val := fmt.Sprintf("%s", value)
|
val := fmt.Sprintf("%s", value)
|
||||||
if name == "component" {
|
if name == "component" {
|
||||||
if value == nil {
|
if value == nil {
|
||||||
|
@ -121,8 +121,8 @@ func (l *Logger) updateLogLevel() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
scopes := strings.Split(strings.ToLower(env), ",")
|
scopes := strings.SplitSeq(strings.ToLower(env), ",")
|
||||||
for _, scope := range scopes {
|
for scope := range scopes {
|
||||||
l.scopeLevels[scope] = level
|
l.scopeLevels[scope] = level
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,32 +13,32 @@ type pionLogger struct {
|
||||||
func (c pionLogger) Trace(msg string) {
|
func (c pionLogger) Trace(msg string) {
|
||||||
c.logger.Trace().Msg(msg)
|
c.logger.Trace().Msg(msg)
|
||||||
}
|
}
|
||||||
func (c pionLogger) Tracef(format string, args ...interface{}) {
|
func (c pionLogger) Tracef(format string, args ...any) {
|
||||||
c.logger.Trace().Msgf(format, args...)
|
c.logger.Trace().Msgf(format, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c pionLogger) Debug(msg string) {
|
func (c pionLogger) Debug(msg string) {
|
||||||
c.logger.Debug().Msg(msg)
|
c.logger.Debug().Msg(msg)
|
||||||
}
|
}
|
||||||
func (c pionLogger) Debugf(format string, args ...interface{}) {
|
func (c pionLogger) Debugf(format string, args ...any) {
|
||||||
c.logger.Debug().Msgf(format, args...)
|
c.logger.Debug().Msgf(format, args...)
|
||||||
}
|
}
|
||||||
func (c pionLogger) Info(msg string) {
|
func (c pionLogger) Info(msg string) {
|
||||||
c.logger.Info().Msg(msg)
|
c.logger.Info().Msg(msg)
|
||||||
}
|
}
|
||||||
func (c pionLogger) Infof(format string, args ...interface{}) {
|
func (c pionLogger) Infof(format string, args ...any) {
|
||||||
c.logger.Info().Msgf(format, args...)
|
c.logger.Info().Msgf(format, args...)
|
||||||
}
|
}
|
||||||
func (c pionLogger) Warn(msg string) {
|
func (c pionLogger) Warn(msg string) {
|
||||||
c.logger.Warn().Msg(msg)
|
c.logger.Warn().Msg(msg)
|
||||||
}
|
}
|
||||||
func (c pionLogger) Warnf(format string, args ...interface{}) {
|
func (c pionLogger) Warnf(format string, args ...any) {
|
||||||
c.logger.Warn().Msgf(format, args...)
|
c.logger.Warn().Msgf(format, args...)
|
||||||
}
|
}
|
||||||
func (c pionLogger) Error(msg string) {
|
func (c pionLogger) Error(msg string) {
|
||||||
c.logger.Error().Msg(msg)
|
c.logger.Error().Msg(msg)
|
||||||
}
|
}
|
||||||
func (c pionLogger) Errorf(format string, args ...interface{}) {
|
func (c pionLogger) Errorf(format string, args ...any) {
|
||||||
c.logger.Error().Msgf(format, args...)
|
c.logger.Error().Msgf(format, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,7 @@ func GetDefaultLogger() *zerolog.Logger {
|
||||||
return &defaultLogger
|
return &defaultLogger
|
||||||
}
|
}
|
||||||
|
|
||||||
func ErrorfL(l *zerolog.Logger, format string, err error, args ...interface{}) error {
|
func ErrorfL(l *zerolog.Logger, format string, err error, args ...any) error {
|
||||||
// TODO: move rootLogger to logging package
|
// TODO: move rootLogger to logging package
|
||||||
if l == nil {
|
if l == nil {
|
||||||
l = &defaultLogger
|
l = &defaultLogger
|
||||||
|
|
|
@ -42,7 +42,7 @@ func updateEtcHosts(hostname string, fqdn string) error {
|
||||||
hostLine := fmt.Sprintf("127.0.1.1\t%s %s", hostname, fqdn)
|
hostLine := fmt.Sprintf("127.0.1.1\t%s %s", hostname, fqdn)
|
||||||
hostLineExists := false
|
hostLineExists := false
|
||||||
|
|
||||||
for _, line := range strings.Split(string(lines), "\n") {
|
for line := range strings.SplitSeq(string(lines), "\n") {
|
||||||
if strings.HasPrefix(line, "127.0.1.1") {
|
if strings.HasPrefix(line, "127.0.1.1") {
|
||||||
hostLineExists = true
|
hostLineExists = true
|
||||||
line = hostLine
|
line = hostLine
|
||||||
|
|
|
@ -13,7 +13,7 @@ func lifetimeToTime(lifetime int) *time.Time {
|
||||||
return &t
|
return &t
|
||||||
}
|
}
|
||||||
|
|
||||||
func IsSame(a, b interface{}) bool {
|
func IsSame(a, b any) bool {
|
||||||
aJSON, err := json.Marshal(a)
|
aJSON, err := json.Marshal(a)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
|
|
|
@ -101,7 +101,7 @@ func (l *Lease) SetLeaseExpiry() (time.Time, error) {
|
||||||
func UnmarshalDHCPCLease(lease *Lease, str string) error {
|
func UnmarshalDHCPCLease(lease *Lease, str string) error {
|
||||||
// parse the lease file as a map
|
// parse the lease file as a map
|
||||||
data := make(map[string]string)
|
data := make(map[string]string)
|
||||||
for _, line := range strings.Split(str, "\n") {
|
for line := range strings.SplitSeq(str, "\n") {
|
||||||
line = strings.TrimSpace(line)
|
line = strings.TrimSpace(line)
|
||||||
// skip empty lines and comments
|
// skip empty lines and comments
|
||||||
if line == "" || strings.HasPrefix(line, "#") {
|
if line == "" || strings.HasPrefix(line, "#") {
|
||||||
|
@ -165,7 +165,7 @@ func UnmarshalDHCPCLease(lease *Lease, str string) error {
|
||||||
field.Set(reflect.ValueOf(ip))
|
field.Set(reflect.ValueOf(ip))
|
||||||
case []net.IP:
|
case []net.IP:
|
||||||
val := make([]net.IP, 0)
|
val := make([]net.IP, 0)
|
||||||
for _, ipStr := range strings.Fields(value) {
|
for ipStr := range strings.FieldsSeq(value) {
|
||||||
ip := net.ParseIP(ipStr)
|
ip := net.ParseIP(ipStr)
|
||||||
if ip == nil {
|
if ip == nil {
|
||||||
continue
|
continue
|
||||||
|
|
|
@ -52,7 +52,7 @@ func NewDHCPClient(options *DHCPClientOptions) *DHCPClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *DHCPClient) getWatchPaths() []string {
|
func (c *DHCPClient) getWatchPaths() []string {
|
||||||
watchPaths := make(map[string]interface{})
|
watchPaths := make(map[string]any)
|
||||||
watchPaths[filepath.Dir(c.leaseFile)] = nil
|
watchPaths[filepath.Dir(c.leaseFile)] = nil
|
||||||
|
|
||||||
if c.pidFile != "" {
|
if c.pidFile != "" {
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
package usbgadget
|
package usbgadget
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -61,6 +61,8 @@ var keyboardReportDesc = []byte{
|
||||||
|
|
||||||
const (
|
const (
|
||||||
hidReadBufferSize = 8
|
hidReadBufferSize = 8
|
||||||
|
hidKeyBufferSize = 6
|
||||||
|
hidErrorRollOver = 0x01
|
||||||
// https://www.usb.org/sites/default/files/documents/hid1_11.pdf
|
// https://www.usb.org/sites/default/files/documents/hid1_11.pdf
|
||||||
// https://www.usb.org/sites/default/files/hut1_2.pdf
|
// https://www.usb.org/sites/default/files/hut1_2.pdf
|
||||||
KeyboardLedMaskNumLock = 1 << 0
|
KeyboardLedMaskNumLock = 1 << 0
|
||||||
|
@ -68,7 +70,9 @@ const (
|
||||||
KeyboardLedMaskScrollLock = 1 << 2
|
KeyboardLedMaskScrollLock = 1 << 2
|
||||||
KeyboardLedMaskCompose = 1 << 3
|
KeyboardLedMaskCompose = 1 << 3
|
||||||
KeyboardLedMaskKana = 1 << 4
|
KeyboardLedMaskKana = 1 << 4
|
||||||
ValidKeyboardLedMasks = KeyboardLedMaskNumLock | KeyboardLedMaskCapsLock | KeyboardLedMaskScrollLock | KeyboardLedMaskCompose | KeyboardLedMaskKana
|
// power on/off LED is 5
|
||||||
|
KeyboardLedMaskShift = 1 << 6
|
||||||
|
ValidKeyboardLedMasks = KeyboardLedMaskNumLock | KeyboardLedMaskCapsLock | KeyboardLedMaskScrollLock | KeyboardLedMaskCompose | KeyboardLedMaskKana | KeyboardLedMaskShift
|
||||||
)
|
)
|
||||||
|
|
||||||
// Synchronization between LED states and CAPS LOCK, NUM LOCK, SCROLL LOCK,
|
// Synchronization between LED states and CAPS LOCK, NUM LOCK, SCROLL LOCK,
|
||||||
|
@ -81,6 +85,7 @@ type KeyboardState struct {
|
||||||
ScrollLock bool `json:"scroll_lock"`
|
ScrollLock bool `json:"scroll_lock"`
|
||||||
Compose bool `json:"compose"`
|
Compose bool `json:"compose"`
|
||||||
Kana bool `json:"kana"`
|
Kana bool `json:"kana"`
|
||||||
|
Shift bool `json:"shift"` // This is not part of the main USB HID spec
|
||||||
}
|
}
|
||||||
|
|
||||||
func getKeyboardState(b byte) KeyboardState {
|
func getKeyboardState(b byte) KeyboardState {
|
||||||
|
@ -91,27 +96,27 @@ func getKeyboardState(b byte) KeyboardState {
|
||||||
ScrollLock: b&KeyboardLedMaskScrollLock != 0,
|
ScrollLock: b&KeyboardLedMaskScrollLock != 0,
|
||||||
Compose: b&KeyboardLedMaskCompose != 0,
|
Compose: b&KeyboardLedMaskCompose != 0,
|
||||||
Kana: b&KeyboardLedMaskKana != 0,
|
Kana: b&KeyboardLedMaskKana != 0,
|
||||||
|
Shift: b&KeyboardLedMaskShift != 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *UsbGadget) updateKeyboardState(b byte) {
|
func (u *UsbGadget) updateKeyboardState(state byte) {
|
||||||
u.keyboardStateLock.Lock()
|
u.keyboardStateLock.Lock()
|
||||||
defer u.keyboardStateLock.Unlock()
|
defer u.keyboardStateLock.Unlock()
|
||||||
|
|
||||||
if b&^ValidKeyboardLedMasks != 0 {
|
if state&^ValidKeyboardLedMasks != 0 {
|
||||||
u.log.Trace().Uint8("b", b).Msg("contains invalid bits, ignoring")
|
u.log.Warn().Uint8("state", state).Msg("ignoring invalid bits")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
newState := getKeyboardState(b)
|
if u.keyboardState == state {
|
||||||
if reflect.DeepEqual(u.keyboardState, newState) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
u.log.Info().Interface("old", u.keyboardState).Interface("new", newState).Msg("keyboardState updated")
|
u.log.Trace().Uint8("old", u.keyboardState).Uint8("new", state).Msg("keyboardState updated")
|
||||||
u.keyboardState = newState
|
u.keyboardState = state
|
||||||
|
|
||||||
if u.onKeyboardStateChange != nil {
|
if u.onKeyboardStateChange != nil {
|
||||||
(*u.onKeyboardStateChange)(newState)
|
(*u.onKeyboardStateChange)(getKeyboardState(state))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -123,7 +128,35 @@ func (u *UsbGadget) GetKeyboardState() KeyboardState {
|
||||||
u.keyboardStateLock.Lock()
|
u.keyboardStateLock.Lock()
|
||||||
defer u.keyboardStateLock.Unlock()
|
defer u.keyboardStateLock.Unlock()
|
||||||
|
|
||||||
return u.keyboardState
|
return getKeyboardState(u.keyboardState)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UsbGadget) GetKeysDownState() KeysDownState {
|
||||||
|
u.keyboardStateLock.Lock()
|
||||||
|
defer u.keyboardStateLock.Unlock()
|
||||||
|
|
||||||
|
return u.keysDownState
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UsbGadget) updateKeyDownState(state KeysDownState) {
|
||||||
|
u.keyboardStateLock.Lock()
|
||||||
|
defer u.keyboardStateLock.Unlock()
|
||||||
|
|
||||||
|
if u.keysDownState.Modifier == state.Modifier &&
|
||||||
|
bytes.Equal(u.keysDownState.Keys, state.Keys) {
|
||||||
|
return // No change in key down state
|
||||||
|
}
|
||||||
|
|
||||||
|
u.log.Trace().Interface("old", u.keysDownState).Interface("new", state).Msg("keysDownState updated")
|
||||||
|
u.keysDownState = state
|
||||||
|
|
||||||
|
if u.onKeysDownChange != nil {
|
||||||
|
(*u.onKeysDownChange)(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UsbGadget) SetOnKeysDownChange(f func(state KeysDownState)) {
|
||||||
|
u.onKeysDownChange = &f
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *UsbGadget) listenKeyboardEvents() {
|
func (u *UsbGadget) listenKeyboardEvents() {
|
||||||
|
@ -142,7 +175,7 @@ func (u *UsbGadget) listenKeyboardEvents() {
|
||||||
l.Info().Msg("context done")
|
l.Info().Msg("context done")
|
||||||
return
|
return
|
||||||
default:
|
default:
|
||||||
l.Trace().Msg("reading from keyboard")
|
l.Trace().Msg("reading from keyboard for LED state changes")
|
||||||
if u.keyboardHidFile == nil {
|
if u.keyboardHidFile == nil {
|
||||||
u.logWithSuppression("keyboardHidFileNil", 100, &l, nil, "keyboardHidFile is nil")
|
u.logWithSuppression("keyboardHidFileNil", 100, &l, nil, "keyboardHidFile is nil")
|
||||||
// show the error every 100 times to avoid spamming the logs
|
// show the error every 100 times to avoid spamming the logs
|
||||||
|
@ -159,7 +192,7 @@ func (u *UsbGadget) listenKeyboardEvents() {
|
||||||
}
|
}
|
||||||
u.resetLogSuppressionCounter("keyboardHidFileRead")
|
u.resetLogSuppressionCounter("keyboardHidFileRead")
|
||||||
|
|
||||||
l.Trace().Int("n", n).Bytes("buf", buf).Msg("got data from keyboard")
|
l.Trace().Int("n", n).Uints8("buf", buf).Msg("got data from keyboard")
|
||||||
if n != 1 {
|
if n != 1 {
|
||||||
l.Trace().Int("n", n).Msg("expected 1 byte, got")
|
l.Trace().Int("n", n).Msg("expected 1 byte, got")
|
||||||
continue
|
continue
|
||||||
|
@ -195,12 +228,12 @@ func (u *UsbGadget) OpenKeyboardHidFile() error {
|
||||||
return u.openKeyboardHidFile()
|
return u.openKeyboardHidFile()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *UsbGadget) keyboardWriteHidFile(data []byte) error {
|
func (u *UsbGadget) keyboardWriteHidFile(modifier byte, keys []byte) error {
|
||||||
if err := u.openKeyboardHidFile(); err != nil {
|
if err := u.openKeyboardHidFile(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := u.keyboardHidFile.Write(data)
|
_, err := u.keyboardHidFile.Write(append([]byte{modifier, 0x00}, keys[:hidKeyBufferSize]...))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
u.logWithSuppression("keyboardWriteHidFile", 100, u.log, err, "failed to write to hidg0")
|
u.logWithSuppression("keyboardWriteHidFile", 100, u.log, err, "failed to write to hidg0")
|
||||||
u.keyboardHidFile.Close()
|
u.keyboardHidFile.Close()
|
||||||
|
@ -211,22 +244,145 @@ func (u *UsbGadget) keyboardWriteHidFile(data []byte) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *UsbGadget) KeyboardReport(modifier uint8, keys []uint8) error {
|
func (u *UsbGadget) UpdateKeysDown(modifier byte, keys []byte) KeysDownState {
|
||||||
|
// if we just reported an error roll over, we should clear the keys
|
||||||
|
if keys[0] == hidErrorRollOver {
|
||||||
|
for i := range keys {
|
||||||
|
keys[i] = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
downState := KeysDownState{
|
||||||
|
Modifier: modifier,
|
||||||
|
Keys: []byte(keys[:]),
|
||||||
|
}
|
||||||
|
u.updateKeyDownState(downState)
|
||||||
|
return downState
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UsbGadget) KeyboardReport(modifier byte, keys []byte) (KeysDownState, error) {
|
||||||
u.keyboardLock.Lock()
|
u.keyboardLock.Lock()
|
||||||
defer u.keyboardLock.Unlock()
|
defer u.keyboardLock.Unlock()
|
||||||
|
defer u.resetUserInputTime()
|
||||||
|
|
||||||
if len(keys) > 6 {
|
if len(keys) > hidKeyBufferSize {
|
||||||
keys = keys[:6]
|
keys = keys[:hidKeyBufferSize]
|
||||||
}
|
}
|
||||||
if len(keys) < 6 {
|
if len(keys) < hidKeyBufferSize {
|
||||||
keys = append(keys, make([]uint8, 6-len(keys))...)
|
keys = append(keys, make([]byte, hidKeyBufferSize-len(keys))...)
|
||||||
}
|
}
|
||||||
|
|
||||||
err := u.keyboardWriteHidFile([]byte{modifier, 0, keys[0], keys[1], keys[2], keys[3], keys[4], keys[5]})
|
err := u.keyboardWriteHidFile(modifier, keys)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
u.log.Warn().Uint8("modifier", modifier).Uints8("keys", keys).Msg("Could not write keyboard report to hidg0")
|
||||||
}
|
}
|
||||||
|
|
||||||
u.resetUserInputTime()
|
return u.UpdateKeysDown(modifier, keys), err
|
||||||
return nil
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
// https://www.usb.org/sites/default/files/documents/hut1_2.pdf
|
||||||
|
// Dynamic Flags (DV)
|
||||||
|
LeftControl = 0xE0
|
||||||
|
LeftShift = 0xE1
|
||||||
|
LeftAlt = 0xE2
|
||||||
|
LeftSuper = 0xE3 // Left GUI (e.g. Windows key, Apple Command key)
|
||||||
|
RightControl = 0xE4
|
||||||
|
RightShift = 0xE5
|
||||||
|
RightAlt = 0xE6
|
||||||
|
RightSuper = 0xE7 // Right GUI (e.g. Windows key, Apple Command key)
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// https://www.usb.org/sites/default/files/documents/hid1_11.pdf Appendix C
|
||||||
|
ModifierMaskLeftControl = 0x01
|
||||||
|
ModifierMaskRightControl = 0x10
|
||||||
|
ModifierMaskLeftShift = 0x02
|
||||||
|
ModifierMaskRightShift = 0x20
|
||||||
|
ModifierMaskLeftAlt = 0x04
|
||||||
|
ModifierMaskRightAlt = 0x40
|
||||||
|
ModifierMaskLeftSuper = 0x08
|
||||||
|
ModifierMaskRightSuper = 0x80
|
||||||
|
)
|
||||||
|
|
||||||
|
// KeyCodeToMaskMap is a slice of KeyCodeMask for quick lookup
|
||||||
|
var KeyCodeToMaskMap = map[byte]byte{
|
||||||
|
LeftControl: ModifierMaskLeftControl,
|
||||||
|
LeftShift: ModifierMaskLeftShift,
|
||||||
|
LeftAlt: ModifierMaskLeftAlt,
|
||||||
|
LeftSuper: ModifierMaskLeftSuper,
|
||||||
|
RightControl: ModifierMaskRightControl,
|
||||||
|
RightShift: ModifierMaskRightShift,
|
||||||
|
RightAlt: ModifierMaskRightAlt,
|
||||||
|
RightSuper: ModifierMaskRightSuper,
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UsbGadget) KeypressReport(key byte, press bool) (KeysDownState, error) {
|
||||||
|
u.keyboardLock.Lock()
|
||||||
|
defer u.keyboardLock.Unlock()
|
||||||
|
defer u.resetUserInputTime()
|
||||||
|
|
||||||
|
// IMPORTANT: This code parallels the logic in the kernel's hid-gadget driver
|
||||||
|
// for handling key presses and releases. It ensures that the USB gadget
|
||||||
|
// behaves similarly to a real USB HID keyboard. This logic is paralleled
|
||||||
|
// in the client/browser-side code in useKeyboard.ts so make sure to keep
|
||||||
|
// them in sync.
|
||||||
|
var state = u.keysDownState
|
||||||
|
modifier := state.Modifier
|
||||||
|
keys := append([]byte(nil), state.Keys...)
|
||||||
|
|
||||||
|
if mask, exists := KeyCodeToMaskMap[key]; exists {
|
||||||
|
// If the key is a modifier key, we update the keyboardModifier state
|
||||||
|
// by setting or clearing the corresponding bit in the modifier byte.
|
||||||
|
// This allows us to track the state of dynamic modifier keys like
|
||||||
|
// Shift, Control, Alt, and Super.
|
||||||
|
if press {
|
||||||
|
modifier |= mask
|
||||||
|
} else {
|
||||||
|
modifier &^= mask
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// handle other keys that are not modifier keys by placing or removing them
|
||||||
|
// from the key buffer since the buffer tracks currently pressed keys
|
||||||
|
overrun := true
|
||||||
|
for i := range hidKeyBufferSize {
|
||||||
|
// If we find the key in the buffer the buffer, we either remove it (if press is false)
|
||||||
|
// or do nothing (if down is true) because the buffer tracks currently pressed keys
|
||||||
|
// and if we find a zero byte, we can place the key there (if press is true)
|
||||||
|
if keys[i] == key || keys[i] == 0 {
|
||||||
|
if press {
|
||||||
|
keys[i] = key // overwrites the zero byte or the same key if already pressed
|
||||||
|
} else {
|
||||||
|
// we are releasing the key, remove it from the buffer
|
||||||
|
if keys[i] != 0 {
|
||||||
|
copy(keys[i:], keys[i+1:])
|
||||||
|
keys[hidKeyBufferSize-1] = 0 // Clear the last byte
|
||||||
|
}
|
||||||
|
}
|
||||||
|
overrun = false // We found a slot for the key
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we reach here it means we didn't find an empty slot or the key in the buffer
|
||||||
|
if overrun {
|
||||||
|
if press {
|
||||||
|
u.log.Error().Uint8("key", key).Msg("keyboard buffer overflow, key not added")
|
||||||
|
// Fill all key slots with ErrorRollOver (0x01) to indicate overflow
|
||||||
|
for i := range keys {
|
||||||
|
keys[i] = hidErrorRollOver
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If we are releasing a key, and we didn't find it in a slot, who cares?
|
||||||
|
u.log.Warn().Uint8("key", key).Msg("key not found in buffer, nothing to release")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err := u.keyboardWriteHidFile(modifier, keys)
|
||||||
|
if err != nil {
|
||||||
|
u.log.Warn().Uint8("modifier", modifier).Uints8("keys", keys).Msg("Could not write keypress report to hidg0")
|
||||||
|
}
|
||||||
|
|
||||||
|
return u.UpdateKeysDown(modifier, keys), err
|
||||||
}
|
}
|
||||||
|
|
|
@ -85,17 +85,17 @@ func (u *UsbGadget) absMouseWriteHidFile(data []byte) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *UsbGadget) AbsMouseReport(x, y int, buttons uint8) error {
|
func (u *UsbGadget) AbsMouseReport(x int, y int, buttons uint8) error {
|
||||||
u.absMouseLock.Lock()
|
u.absMouseLock.Lock()
|
||||||
defer u.absMouseLock.Unlock()
|
defer u.absMouseLock.Unlock()
|
||||||
|
|
||||||
err := u.absMouseWriteHidFile([]byte{
|
err := u.absMouseWriteHidFile([]byte{
|
||||||
1, // Report ID 1
|
1, // Report ID 1
|
||||||
buttons, // Buttons
|
buttons, // Buttons
|
||||||
uint8(x), // X Low Byte
|
byte(x), // X Low Byte
|
||||||
uint8(x >> 8), // X High Byte
|
byte(x >> 8), // X High Byte
|
||||||
uint8(y), // Y Low Byte
|
byte(y), // Y Low Byte
|
||||||
uint8(y >> 8), // Y High Byte
|
byte(y >> 8), // Y High Byte
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -75,14 +75,14 @@ func (u *UsbGadget) relMouseWriteHidFile(data []byte) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *UsbGadget) RelMouseReport(mx, my int8, buttons uint8) error {
|
func (u *UsbGadget) RelMouseReport(mx int8, my int8, buttons uint8) error {
|
||||||
u.relMouseLock.Lock()
|
u.relMouseLock.Lock()
|
||||||
defer u.relMouseLock.Unlock()
|
defer u.relMouseLock.Unlock()
|
||||||
|
|
||||||
err := u.relMouseWriteHidFile([]byte{
|
err := u.relMouseWriteHidFile([]byte{
|
||||||
buttons, // Buttons
|
buttons, // Buttons
|
||||||
uint8(mx), // X
|
byte(mx), // X
|
||||||
uint8(my), // Y
|
byte(my), // Y
|
||||||
0, // Wheel
|
0, // Wheel
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -41,6 +41,11 @@ var defaultUsbGadgetDevices = Devices{
|
||||||
MassStorage: true,
|
MassStorage: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type KeysDownState struct {
|
||||||
|
Modifier byte `json:"modifier"`
|
||||||
|
Keys ByteSlice `json:"keys"`
|
||||||
|
}
|
||||||
|
|
||||||
// UsbGadget is a struct that represents a USB gadget.
|
// UsbGadget is a struct that represents a USB gadget.
|
||||||
type UsbGadget struct {
|
type UsbGadget struct {
|
||||||
name string
|
name string
|
||||||
|
@ -60,7 +65,9 @@ type UsbGadget struct {
|
||||||
relMouseHidFile *os.File
|
relMouseHidFile *os.File
|
||||||
relMouseLock sync.Mutex
|
relMouseLock sync.Mutex
|
||||||
|
|
||||||
keyboardState KeyboardState
|
keyboardState byte // keyboard latched state (NumLock, CapsLock, ScrollLock, Compose, Kana)
|
||||||
|
keysDownState KeysDownState // keyboard dynamic state (modifier keys and pressed keys)
|
||||||
|
|
||||||
keyboardStateLock sync.Mutex
|
keyboardStateLock sync.Mutex
|
||||||
keyboardStateCtx context.Context
|
keyboardStateCtx context.Context
|
||||||
keyboardStateCancel context.CancelFunc
|
keyboardStateCancel context.CancelFunc
|
||||||
|
@ -77,6 +84,7 @@ type UsbGadget struct {
|
||||||
txLock sync.Mutex
|
txLock sync.Mutex
|
||||||
|
|
||||||
onKeyboardStateChange *func(state KeyboardState)
|
onKeyboardStateChange *func(state KeyboardState)
|
||||||
|
onKeysDownChange *func(state KeysDownState)
|
||||||
|
|
||||||
log *zerolog.Logger
|
log *zerolog.Logger
|
||||||
|
|
||||||
|
@ -122,7 +130,8 @@ func newUsbGadget(name string, configMap map[string]gadgetConfigItem, enabledDev
|
||||||
txLock: sync.Mutex{},
|
txLock: sync.Mutex{},
|
||||||
keyboardStateCtx: keyboardCtx,
|
keyboardStateCtx: keyboardCtx,
|
||||||
keyboardStateCancel: keyboardCancel,
|
keyboardStateCancel: keyboardCancel,
|
||||||
keyboardState: KeyboardState{},
|
keyboardState: 0,
|
||||||
|
keysDownState: KeysDownState{Modifier: 0, Keys: []byte{0, 0, 0, 0, 0, 0}}, // must be initialized to hidKeyBufferSize (6) zero bytes
|
||||||
enabledDevices: *enabledDevices,
|
enabledDevices: *enabledDevices,
|
||||||
lastUserInput: time.Now(),
|
lastUserInput: time.Now(),
|
||||||
log: logger,
|
log: logger,
|
||||||
|
|
|
@ -2,6 +2,7 @@ package usbgadget
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
@ -10,6 +11,31 @@ import (
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type ByteSlice []byte
|
||||||
|
|
||||||
|
func (s ByteSlice) MarshalJSON() ([]byte, error) {
|
||||||
|
vals := make([]int, len(s))
|
||||||
|
for i, v := range s {
|
||||||
|
vals[i] = int(v)
|
||||||
|
}
|
||||||
|
return json.Marshal(vals)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ByteSlice) UnmarshalJSON(data []byte) error {
|
||||||
|
var vals []int
|
||||||
|
if err := json.Unmarshal(data, &vals); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*s = make([]byte, len(vals))
|
||||||
|
for i, v := range vals {
|
||||||
|
if v < 0 || v > 255 {
|
||||||
|
return fmt.Errorf("value %d out of byte range", v)
|
||||||
|
}
|
||||||
|
(*s)[i] = byte(v)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func joinPath(basePath string, paths []string) string {
|
func joinPath(basePath string, paths []string) string {
|
||||||
pathArr := append([]string{basePath}, paths...)
|
pathArr := append([]string{basePath}, paths...)
|
||||||
return filepath.Join(pathArr...)
|
return filepath.Join(pathArr...)
|
||||||
|
@ -81,7 +107,7 @@ func compareFileContent(oldContent []byte, newContent []byte, looserMatch bool)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *UsbGadget) logWithSuppression(counterName string, every int, logger *zerolog.Logger, err error, msg string, args ...interface{}) {
|
func (u *UsbGadget) logWithSuppression(counterName string, every int, logger *zerolog.Logger, err error, msg string, args ...any) {
|
||||||
u.logSuppressionLock.Lock()
|
u.logSuppressionLock.Lock()
|
||||||
defer u.logSuppressionLock.Unlock()
|
defer u.logSuppressionLock.Unlock()
|
||||||
|
|
||||||
|
|
106
jsonrpc.go
106
jsonrpc.go
|
@ -13,6 +13,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pion/webrtc/v4"
|
"github.com/pion/webrtc/v4"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
"go.bug.st/serial"
|
"go.bug.st/serial"
|
||||||
|
|
||||||
"github.com/jetkvm/kvm/internal/usbgadget"
|
"github.com/jetkvm/kvm/internal/usbgadget"
|
||||||
|
@ -21,21 +22,21 @@ import (
|
||||||
type JSONRPCRequest struct {
|
type JSONRPCRequest struct {
|
||||||
JSONRPC string `json:"jsonrpc"`
|
JSONRPC string `json:"jsonrpc"`
|
||||||
Method string `json:"method"`
|
Method string `json:"method"`
|
||||||
Params map[string]interface{} `json:"params,omitempty"`
|
Params map[string]any `json:"params,omitempty"`
|
||||||
ID interface{} `json:"id,omitempty"`
|
ID any `json:"id,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type JSONRPCResponse struct {
|
type JSONRPCResponse struct {
|
||||||
JSONRPC string `json:"jsonrpc"`
|
JSONRPC string `json:"jsonrpc"`
|
||||||
Result interface{} `json:"result,omitempty"`
|
Result any `json:"result,omitempty"`
|
||||||
Error interface{} `json:"error,omitempty"`
|
Error any `json:"error,omitempty"`
|
||||||
ID interface{} `json:"id"`
|
ID any `json:"id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type JSONRPCEvent struct {
|
type JSONRPCEvent struct {
|
||||||
JSONRPC string `json:"jsonrpc"`
|
JSONRPC string `json:"jsonrpc"`
|
||||||
Method string `json:"method"`
|
Method string `json:"method"`
|
||||||
Params interface{} `json:"params,omitempty"`
|
Params any `json:"params,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DisplayRotationSettings struct {
|
type DisplayRotationSettings struct {
|
||||||
|
@ -61,7 +62,7 @@ func writeJSONRPCResponse(response JSONRPCResponse, session *Session) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeJSONRPCEvent(event string, params interface{}, session *Session) {
|
func writeJSONRPCEvent(event string, params any, session *Session) {
|
||||||
request := JSONRPCEvent{
|
request := JSONRPCEvent{
|
||||||
JSONRPC: "2.0",
|
JSONRPC: "2.0",
|
||||||
Method: event,
|
Method: event,
|
||||||
|
@ -102,7 +103,7 @@ func onRPCMessage(message webrtc.DataChannelMessage, session *Session) {
|
||||||
|
|
||||||
errorResponse := JSONRPCResponse{
|
errorResponse := JSONRPCResponse{
|
||||||
JSONRPC: "2.0",
|
JSONRPC: "2.0",
|
||||||
Error: map[string]interface{}{
|
Error: map[string]any{
|
||||||
"code": -32700,
|
"code": -32700,
|
||||||
"message": "Parse error",
|
"message": "Parse error",
|
||||||
},
|
},
|
||||||
|
@ -123,7 +124,7 @@ func onRPCMessage(message webrtc.DataChannelMessage, session *Session) {
|
||||||
if !ok {
|
if !ok {
|
||||||
errorResponse := JSONRPCResponse{
|
errorResponse := JSONRPCResponse{
|
||||||
JSONRPC: "2.0",
|
JSONRPC: "2.0",
|
||||||
Error: map[string]interface{}{
|
Error: map[string]any{
|
||||||
"code": -32601,
|
"code": -32601,
|
||||||
"message": "Method not found",
|
"message": "Method not found",
|
||||||
},
|
},
|
||||||
|
@ -133,13 +134,12 @@ func onRPCMessage(message webrtc.DataChannelMessage, session *Session) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
scopedLogger.Trace().Msg("Calling RPC handler")
|
result, err := callRPCHandler(scopedLogger, handler, request.Params)
|
||||||
result, err := callRPCHandler(handler, request.Params)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
scopedLogger.Error().Err(err).Msg("Error calling RPC handler")
|
scopedLogger.Error().Err(err).Msg("Error calling RPC handler")
|
||||||
errorResponse := JSONRPCResponse{
|
errorResponse := JSONRPCResponse{
|
||||||
JSONRPC: "2.0",
|
JSONRPC: "2.0",
|
||||||
Error: map[string]interface{}{
|
Error: map[string]any{
|
||||||
"code": -32603,
|
"code": -32603,
|
||||||
"message": "Internal error",
|
"message": "Internal error",
|
||||||
"data": err.Error(),
|
"data": err.Error(),
|
||||||
|
@ -200,7 +200,7 @@ func rpcGetStreamQualityFactor() (float64, error) {
|
||||||
|
|
||||||
func rpcSetStreamQualityFactor(factor float64) error {
|
func rpcSetStreamQualityFactor(factor float64) error {
|
||||||
logger.Info().Float64("factor", factor).Msg("Setting stream quality factor")
|
logger.Info().Float64("factor", factor).Msg("Setting stream quality factor")
|
||||||
var _, err = CallCtrlAction("set_video_quality_factor", map[string]interface{}{"quality_factor": factor})
|
var _, err = CallCtrlAction("set_video_quality_factor", map[string]any{"quality_factor": factor})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -240,7 +240,7 @@ func rpcSetEDID(edid string) error {
|
||||||
} else {
|
} else {
|
||||||
logger.Info().Str("edid", edid).Msg("Setting EDID")
|
logger.Info().Str("edid", edid).Msg("Setting EDID")
|
||||||
}
|
}
|
||||||
_, err := CallCtrlAction("set_edid", map[string]interface{}{"edid": edid})
|
_, err := CallCtrlAction("set_edid", map[string]any{"edid": edid})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -467,12 +467,12 @@ func rpcSetTLSState(state TLSState) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
type RPCHandler struct {
|
type RPCHandler struct {
|
||||||
Func interface{}
|
Func any
|
||||||
Params []string
|
Params []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// call the handler but recover from a panic to ensure our RPC thread doesn't collapse on malformed calls
|
// call the handler but recover from a panic to ensure our RPC thread doesn't collapse on malformed calls
|
||||||
func callRPCHandler(handler RPCHandler, params map[string]interface{}) (result interface{}, err error) {
|
func callRPCHandler(logger zerolog.Logger, handler RPCHandler, params map[string]any) (result any, err error) {
|
||||||
// Use defer to recover from a panic
|
// Use defer to recover from a panic
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
|
@ -486,11 +486,11 @@ func callRPCHandler(handler RPCHandler, params map[string]interface{}) (result i
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Call the handler
|
// Call the handler
|
||||||
result, err = riskyCallRPCHandler(handler, params)
|
result, err = riskyCallRPCHandler(logger, handler, params)
|
||||||
return result, err
|
return result, err // do not combine these two lines into one, as it breaks the above defer function's setting of err
|
||||||
}
|
}
|
||||||
|
|
||||||
func riskyCallRPCHandler(handler RPCHandler, params map[string]interface{}) (interface{}, error) {
|
func riskyCallRPCHandler(logger zerolog.Logger, handler RPCHandler, params map[string]any) (any, error) {
|
||||||
handlerValue := reflect.ValueOf(handler.Func)
|
handlerValue := reflect.ValueOf(handler.Func)
|
||||||
handlerType := handlerValue.Type()
|
handlerType := handlerValue.Type()
|
||||||
|
|
||||||
|
@ -499,20 +499,24 @@ func riskyCallRPCHandler(handler RPCHandler, params map[string]interface{}) (int
|
||||||
}
|
}
|
||||||
|
|
||||||
numParams := handlerType.NumIn()
|
numParams := handlerType.NumIn()
|
||||||
args := make([]reflect.Value, numParams)
|
paramNames := handler.Params // Get the parameter names from the RPCHandler
|
||||||
// Get the parameter names from the RPCHandler
|
|
||||||
paramNames := handler.Params
|
|
||||||
|
|
||||||
if len(paramNames) != numParams {
|
if len(paramNames) != numParams {
|
||||||
return nil, errors.New("mismatch between handler parameters and defined parameter names")
|
err := fmt.Errorf("mismatch between handler parameters (%d) and defined parameter names (%d)", numParams, len(paramNames))
|
||||||
|
logger.Error().Strs("paramNames", paramNames).Err(err).Msg("Cannot call RPC handler")
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := 0; i < numParams; i++ {
|
args := make([]reflect.Value, numParams)
|
||||||
|
|
||||||
|
for i := range numParams {
|
||||||
paramType := handlerType.In(i)
|
paramType := handlerType.In(i)
|
||||||
paramName := paramNames[i]
|
paramName := paramNames[i]
|
||||||
paramValue, ok := params[paramName]
|
paramValue, ok := params[paramName]
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, errors.New("missing parameter: " + paramName)
|
err := fmt.Errorf("missing parameter: %s", paramName)
|
||||||
|
logger.Error().Err(err).Msg("Cannot marshal arguments for RPC handler")
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
convertedValue := reflect.ValueOf(paramValue)
|
convertedValue := reflect.ValueOf(paramValue)
|
||||||
|
@ -529,7 +533,7 @@ func riskyCallRPCHandler(handler RPCHandler, params map[string]interface{}) (int
|
||||||
if elemValue.Kind() == reflect.Float64 && paramType.Elem().Kind() == reflect.Uint8 {
|
if elemValue.Kind() == reflect.Float64 && paramType.Elem().Kind() == reflect.Uint8 {
|
||||||
intValue := int(elemValue.Float())
|
intValue := int(elemValue.Float())
|
||||||
if intValue < 0 || intValue > 255 {
|
if intValue < 0 || intValue > 255 {
|
||||||
return nil, fmt.Errorf("value out of range for uint8: %v", intValue)
|
return nil, fmt.Errorf("value out of range for uint8: %v for parameter %s", intValue, paramName)
|
||||||
}
|
}
|
||||||
newSlice.Index(j).SetUint(uint64(intValue))
|
newSlice.Index(j).SetUint(uint64(intValue))
|
||||||
} else {
|
} else {
|
||||||
|
@ -545,12 +549,12 @@ func riskyCallRPCHandler(handler RPCHandler, params map[string]interface{}) (int
|
||||||
} else if paramType.Kind() == reflect.Struct && convertedValue.Kind() == reflect.Map {
|
} else if paramType.Kind() == reflect.Struct && convertedValue.Kind() == reflect.Map {
|
||||||
jsonData, err := json.Marshal(convertedValue.Interface())
|
jsonData, err := json.Marshal(convertedValue.Interface())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to marshal map to JSON: %v", err)
|
return nil, fmt.Errorf("failed to marshal map to JSON: %v for parameter %s", err, paramName)
|
||||||
}
|
}
|
||||||
|
|
||||||
newStruct := reflect.New(paramType).Interface()
|
newStruct := reflect.New(paramType).Interface()
|
||||||
if err := json.Unmarshal(jsonData, newStruct); err != nil {
|
if err := json.Unmarshal(jsonData, newStruct); err != nil {
|
||||||
return nil, fmt.Errorf("failed to unmarshal JSON into struct: %v", err)
|
return nil, fmt.Errorf("failed to unmarshal JSON into struct: %v for parameter %s", err, paramName)
|
||||||
}
|
}
|
||||||
args[i] = reflect.ValueOf(newStruct).Elem()
|
args[i] = reflect.ValueOf(newStruct).Elem()
|
||||||
} else {
|
} else {
|
||||||
|
@ -561,6 +565,7 @@ func riskyCallRPCHandler(handler RPCHandler, params map[string]interface{}) (int
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.Trace().Msg("Calling RPC handler")
|
||||||
results := handlerValue.Call(args)
|
results := handlerValue.Call(args)
|
||||||
|
|
||||||
if len(results) == 0 {
|
if len(results) == 0 {
|
||||||
|
@ -568,23 +573,32 @@ func riskyCallRPCHandler(handler RPCHandler, params map[string]interface{}) (int
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(results) == 1 {
|
if len(results) == 1 {
|
||||||
if results[0].Type().Implements(reflect.TypeOf((*error)(nil)).Elem()) {
|
if ok, err := asError(results[0]); ok {
|
||||||
if !results[0].IsNil() {
|
return nil, err
|
||||||
return nil, results[0].Interface().(error)
|
|
||||||
}
|
|
||||||
return nil, nil
|
|
||||||
}
|
}
|
||||||
return results[0].Interface(), nil
|
return results[0].Interface(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(results) == 2 && results[1].Type().Implements(reflect.TypeOf((*error)(nil)).Elem()) {
|
if len(results) == 2 {
|
||||||
if !results[1].IsNil() {
|
if ok, err := asError(results[1]); ok {
|
||||||
return nil, results[1].Interface().(error)
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return results[0].Interface(), nil
|
return results[0].Interface(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, errors.New("unexpected return values from handler")
|
return nil, fmt.Errorf("too many return values from handler: %d", len(results))
|
||||||
|
}
|
||||||
|
|
||||||
|
func asError(value reflect.Value) (bool, error) {
|
||||||
|
if value.Type().Implements(reflect.TypeOf((*error)(nil)).Elem()) {
|
||||||
|
if value.IsNil() {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
return true, value.Interface().(error)
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func rpcSetMassStorageMode(mode string) (string, error) {
|
func rpcSetMassStorageMode(mode string) (string, error) {
|
||||||
|
@ -923,7 +937,7 @@ func rpcSetKeyboardLayout(layout string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getKeyboardMacros() (interface{}, error) {
|
func getKeyboardMacros() (any, error) {
|
||||||
macros := make([]KeyboardMacro, len(config.KeyboardMacros))
|
macros := make([]KeyboardMacro, len(config.KeyboardMacros))
|
||||||
copy(macros, config.KeyboardMacros)
|
copy(macros, config.KeyboardMacros)
|
||||||
|
|
||||||
|
@ -931,10 +945,10 @@ func getKeyboardMacros() (interface{}, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
type KeyboardMacrosParams struct {
|
type KeyboardMacrosParams struct {
|
||||||
Macros []interface{} `json:"macros"`
|
Macros []any `json:"macros"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func setKeyboardMacros(params KeyboardMacrosParams) (interface{}, error) {
|
func setKeyboardMacros(params KeyboardMacrosParams) (any, error) {
|
||||||
if params.Macros == nil {
|
if params.Macros == nil {
|
||||||
return nil, fmt.Errorf("missing or invalid macros parameter")
|
return nil, fmt.Errorf("missing or invalid macros parameter")
|
||||||
}
|
}
|
||||||
|
@ -942,7 +956,7 @@ func setKeyboardMacros(params KeyboardMacrosParams) (interface{}, error) {
|
||||||
newMacros := make([]KeyboardMacro, 0, len(params.Macros))
|
newMacros := make([]KeyboardMacro, 0, len(params.Macros))
|
||||||
|
|
||||||
for i, item := range params.Macros {
|
for i, item := range params.Macros {
|
||||||
macroMap, ok := item.(map[string]interface{})
|
macroMap, ok := item.(map[string]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("invalid macro at index %d", i)
|
return nil, fmt.Errorf("invalid macro at index %d", i)
|
||||||
}
|
}
|
||||||
|
@ -960,16 +974,16 @@ func setKeyboardMacros(params KeyboardMacrosParams) (interface{}, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
steps := []KeyboardMacroStep{}
|
steps := []KeyboardMacroStep{}
|
||||||
if stepsArray, ok := macroMap["steps"].([]interface{}); ok {
|
if stepsArray, ok := macroMap["steps"].([]any); ok {
|
||||||
for _, stepItem := range stepsArray {
|
for _, stepItem := range stepsArray {
|
||||||
stepMap, ok := stepItem.(map[string]interface{})
|
stepMap, ok := stepItem.(map[string]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
step := KeyboardMacroStep{}
|
step := KeyboardMacroStep{}
|
||||||
|
|
||||||
if keysArray, ok := stepMap["keys"].([]interface{}); ok {
|
if keysArray, ok := stepMap["keys"].([]any); ok {
|
||||||
for _, k := range keysArray {
|
for _, k := range keysArray {
|
||||||
if keyStr, ok := k.(string); ok {
|
if keyStr, ok := k.(string); ok {
|
||||||
step.Keys = append(step.Keys, keyStr)
|
step.Keys = append(step.Keys, keyStr)
|
||||||
|
@ -977,7 +991,7 @@ func setKeyboardMacros(params KeyboardMacrosParams) (interface{}, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if modsArray, ok := stepMap["modifiers"].([]interface{}); ok {
|
if modsArray, ok := stepMap["modifiers"].([]any); ok {
|
||||||
for _, m := range modsArray {
|
for _, m := range modsArray {
|
||||||
if modStr, ok := m.(string); ok {
|
if modStr, ok := m.(string); ok {
|
||||||
step.Modifiers = append(step.Modifiers, modStr)
|
step.Modifiers = append(step.Modifiers, modStr)
|
||||||
|
@ -1047,6 +1061,8 @@ var rpcHandlers = map[string]RPCHandler{
|
||||||
"renewDHCPLease": {Func: rpcRenewDHCPLease},
|
"renewDHCPLease": {Func: rpcRenewDHCPLease},
|
||||||
"keyboardReport": {Func: rpcKeyboardReport, Params: []string{"modifier", "keys"}},
|
"keyboardReport": {Func: rpcKeyboardReport, Params: []string{"modifier", "keys"}},
|
||||||
"getKeyboardLedState": {Func: rpcGetKeyboardLedState},
|
"getKeyboardLedState": {Func: rpcGetKeyboardLedState},
|
||||||
|
"keypressReport": {Func: rpcKeypressReport, Params: []string{"key", "press"}},
|
||||||
|
"getKeyDownState": {Func: rpcGetKeysDownState},
|
||||||
"absMouseReport": {Func: rpcAbsMouseReport, Params: []string{"x", "y", "buttons"}},
|
"absMouseReport": {Func: rpcAbsMouseReport, Params: []string{"x", "y", "buttons"}},
|
||||||
"relMouseReport": {Func: rpcRelMouseReport, Params: []string{"dx", "dy", "buttons"}},
|
"relMouseReport": {Func: rpcRelMouseReport, Params: []string{"dx", "dy", "buttons"}},
|
||||||
"wheelReport": {Func: rpcWheelReport, Params: []string{"wheelY"}},
|
"wheelReport": {Func: rpcWheelReport, Params: []string{"wheelY"}},
|
||||||
|
|
2
log.go
2
log.go
|
@ -5,7 +5,7 @@ import (
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ErrorfL(l *zerolog.Logger, format string, err error, args ...interface{}) error {
|
func ErrorfL(l *zerolog.Logger, format string, err error, args ...any) error {
|
||||||
return logging.ErrorfL(l, format, err, args...)
|
return logging.ErrorfL(l, format, err, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,14 +23,14 @@ var ctrlSocketConn net.Conn
|
||||||
type CtrlAction struct {
|
type CtrlAction struct {
|
||||||
Action string `json:"action"`
|
Action string `json:"action"`
|
||||||
Seq int32 `json:"seq,omitempty"`
|
Seq int32 `json:"seq,omitempty"`
|
||||||
Params map[string]interface{} `json:"params,omitempty"`
|
Params map[string]any `json:"params,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CtrlResponse struct {
|
type CtrlResponse struct {
|
||||||
Seq int32 `json:"seq,omitempty"`
|
Seq int32 `json:"seq,omitempty"`
|
||||||
Error string `json:"error,omitempty"`
|
Error string `json:"error,omitempty"`
|
||||||
Errno int32 `json:"errno,omitempty"`
|
Errno int32 `json:"errno,omitempty"`
|
||||||
Result map[string]interface{} `json:"result,omitempty"`
|
Result map[string]any `json:"result,omitempty"`
|
||||||
Event string `json:"event,omitempty"`
|
Event string `json:"event,omitempty"`
|
||||||
Data json.RawMessage `json:"data,omitempty"`
|
Data json.RawMessage `json:"data,omitempty"`
|
||||||
}
|
}
|
||||||
|
@ -48,7 +48,7 @@ var (
|
||||||
nativeCmdLock = &sync.Mutex{}
|
nativeCmdLock = &sync.Mutex{}
|
||||||
)
|
)
|
||||||
|
|
||||||
func CallCtrlAction(action string, params map[string]interface{}) (*CtrlResponse, error) {
|
func CallCtrlAction(action string, params map[string]any) (*CtrlResponse, error) {
|
||||||
lock.Lock()
|
lock.Lock()
|
||||||
defer lock.Unlock()
|
defer lock.Unlock()
|
||||||
ctrlAction := CtrlAction{
|
ctrlAction := CtrlAction{
|
||||||
|
@ -429,7 +429,7 @@ func ensureBinaryUpdated(destPath string) error {
|
||||||
func restoreHdmiEdid() {
|
func restoreHdmiEdid() {
|
||||||
if config.EdidString != "" {
|
if config.EdidString != "" {
|
||||||
nativeLogger.Info().Str("edid", config.EdidString).Msg("Restoring HDMI EDID")
|
nativeLogger.Info().Str("edid", config.EdidString).Msg("Restoring HDMI EDID")
|
||||||
_, err := CallCtrlAction("set_edid", map[string]interface{}{"edid": config.EdidString})
|
_, err := CallCtrlAction("set_edid", map[string]any{"edid": config.EdidString})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
nativeLogger.Warn().Err(err).Msg("Failed to restore HDMI EDID")
|
nativeLogger.Warn().Err(err).Msg("Failed to restore HDMI EDID")
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,10 +27,7 @@ func (w *WebRTCDiskReader) Read(ctx context.Context, offset int64, size int64) (
|
||||||
}
|
}
|
||||||
mountedImageSize := currentVirtualMediaState.Size
|
mountedImageSize := currentVirtualMediaState.Size
|
||||||
virtualMediaStateMutex.RUnlock()
|
virtualMediaStateMutex.RUnlock()
|
||||||
end := offset + size
|
end := min(offset+size, mountedImageSize)
|
||||||
if end > mountedImageSize {
|
|
||||||
end = mountedImageSize
|
|
||||||
}
|
|
||||||
req := DiskReadRequest{
|
req := DiskReadRequest{
|
||||||
Start: uint64(offset),
|
Start: uint64(offset),
|
||||||
End: uint64(end),
|
End: uint64(end),
|
||||||
|
|
|
@ -66,6 +66,10 @@ module.exports = defineConfig([{
|
||||||
groups: ["builtin", "external", "internal", "parent", "sibling"],
|
groups: ["builtin", "external", "internal", "parent", "sibling"],
|
||||||
"newlines-between": "always",
|
"newlines-between": "always",
|
||||||
}],
|
}],
|
||||||
|
|
||||||
|
"@typescript-eslint/no-unused-vars": ["warn", {
|
||||||
|
"argsIgnorePattern": "^_", "varsIgnorePattern": "^_"
|
||||||
|
}],
|
||||||
},
|
},
|
||||||
|
|
||||||
settings: {
|
settings: {
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "kvm-ui",
|
"name": "kvm-ui",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "2025.08.07.001",
|
"version": "2025.08.15.2119",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "22.15.0"
|
"node": "22.15.0"
|
||||||
|
@ -39,10 +39,10 @@
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-animate-height": "^3.2.3",
|
"react-animate-height": "^3.2.3",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
"react-hot-toast": "^2.5.2",
|
"react-hot-toast": "^2.6.0",
|
||||||
"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.106",
|
"react-simple-keyboard": "^3.8.111",
|
||||||
"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",
|
||||||
|
@ -52,22 +52,22 @@
|
||||||
"zustand": "^4.5.2"
|
"zustand": "^4.5.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/compat": "^1.3.1",
|
"@eslint/compat": "^1.3.2",
|
||||||
"@eslint/eslintrc": "^3.3.1",
|
"@eslint/eslintrc": "^3.3.1",
|
||||||
"@eslint/js": "^9.32.0",
|
"@eslint/js": "^9.33.0",
|
||||||
"@tailwindcss/forms": "^0.5.10",
|
"@tailwindcss/forms": "^0.5.10",
|
||||||
"@tailwindcss/postcss": "^4.1.11",
|
"@tailwindcss/postcss": "^4.1.12",
|
||||||
"@tailwindcss/typography": "^0.5.16",
|
"@tailwindcss/typography": "^0.5.16",
|
||||||
"@tailwindcss/vite": "^4.1.11",
|
"@tailwindcss/vite": "^4.1.12",
|
||||||
"@types/react": "^19.1.9",
|
"@types/react": "^19.1.10",
|
||||||
"@types/react-dom": "^19.1.7",
|
"@types/react-dom": "^19.1.7",
|
||||||
"@types/semver": "^7.7.0",
|
"@types/semver": "^7.7.0",
|
||||||
"@types/validator": "^13.15.2",
|
"@types/validator": "^13.15.2",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.39.0",
|
"@typescript-eslint/eslint-plugin": "^8.39.1",
|
||||||
"@typescript-eslint/parser": "^8.39.0",
|
"@typescript-eslint/parser": "^8.39.1",
|
||||||
"@vitejs/plugin-react-swc": "^3.10.2",
|
"@vitejs/plugin-react-swc": "^3.10.2",
|
||||||
"autoprefixer": "^10.4.21",
|
"autoprefixer": "^10.4.21",
|
||||||
"eslint": "^9.32.0",
|
"eslint": "^9.33.0",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"eslint-plugin-import": "^2.32.0",
|
"eslint-plugin-import": "^2.32.0",
|
||||||
"eslint-plugin-react": "^7.37.5",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
|
@ -77,7 +77,7 @@
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.14",
|
"prettier-plugin-tailwindcss": "^0.6.14",
|
||||||
"tailwindcss": "^4.1.11",
|
"tailwindcss": "^4.1.12",
|
||||||
"typescript": "^5.9.2",
|
"typescript": "^5.9.2",
|
||||||
"vite": "^6.3.5",
|
"vite": "^6.3.5",
|
||||||
"vite-tsconfig-paths": "^5.1.4"
|
"vite-tsconfig-paths": "^5.1.4"
|
||||||
|
|
|
@ -26,17 +26,13 @@ export default function Actionbar({
|
||||||
requestFullscreen: () => Promise<void>;
|
requestFullscreen: () => Promise<void>;
|
||||||
}) {
|
}) {
|
||||||
const { navigateTo } = useDeviceUiNavigation();
|
const { navigateTo } = useDeviceUiNavigation();
|
||||||
const virtualKeyboard = useHidStore(state => state.isVirtualKeyboardEnabled);
|
const { isVirtualKeyboardEnabled, setVirtualKeyboardEnabled } = useHidStore();
|
||||||
|
const { setDisableVideoFocusTrap, terminalType, setTerminalType, toggleSidebarView } = useUiStore();
|
||||||
|
|
||||||
const setVirtualKeyboard = useHidStore(state => state.setVirtualKeyboardEnabled);
|
|
||||||
const toggleSidebarView = useUiStore(state => state.toggleSidebarView);
|
|
||||||
const setDisableFocusTrap = useUiStore(state => state.setDisableVideoFocusTrap);
|
|
||||||
const terminalType = useUiStore(state => state.terminalType);
|
|
||||||
const setTerminalType = useUiStore(state => state.setTerminalType);
|
|
||||||
const remoteVirtualMediaState = useMountMediaStore(
|
const remoteVirtualMediaState = useMountMediaStore(
|
||||||
state => state.remoteVirtualMediaState,
|
state => state.remoteVirtualMediaState,
|
||||||
);
|
);
|
||||||
const developerMode = useSettingsStore(state => state.developerMode);
|
const { developerMode } = useSettingsStore();
|
||||||
|
|
||||||
// This is the only way to get a reliable state change for the popover
|
// This is the only way to get a reliable state change for the popover
|
||||||
// at time of writing this there is no mount, or unmount event for the popover
|
// at time of writing this there is no mount, or unmount event for the popover
|
||||||
|
@ -47,13 +43,13 @@ export default function Actionbar({
|
||||||
isOpen.current = open;
|
isOpen.current = open;
|
||||||
if (!open) {
|
if (!open) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setDisableFocusTrap(false);
|
setDisableVideoFocusTrap(false);
|
||||||
console.log("Popover is closing. Returning focus trap to video");
|
console.debug("Popover is closing. Returning focus trap to video");
|
||||||
}, 0);
|
}, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[setDisableFocusTrap],
|
[setDisableVideoFocusTrap],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -81,7 +77,7 @@ export default function Actionbar({
|
||||||
text="Paste text"
|
text="Paste text"
|
||||||
LeadingIcon={MdOutlineContentPasteGo}
|
LeadingIcon={MdOutlineContentPasteGo}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setDisableFocusTrap(true);
|
setDisableVideoFocusTrap(true);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</PopoverButton>
|
</PopoverButton>
|
||||||
|
@ -123,7 +119,7 @@ export default function Actionbar({
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setDisableFocusTrap(true);
|
setDisableVideoFocusTrap(true);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</PopoverButton>
|
</PopoverButton>
|
||||||
|
@ -154,7 +150,7 @@ export default function Actionbar({
|
||||||
theme="light"
|
theme="light"
|
||||||
text="Wake on LAN"
|
text="Wake on LAN"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setDisableFocusTrap(true);
|
setDisableVideoFocusTrap(true);
|
||||||
}}
|
}}
|
||||||
LeadingIcon={({ className }) => (
|
LeadingIcon={({ className }) => (
|
||||||
<svg
|
<svg
|
||||||
|
@ -204,7 +200,7 @@ export default function Actionbar({
|
||||||
theme="light"
|
theme="light"
|
||||||
text="Virtual Keyboard"
|
text="Virtual Keyboard"
|
||||||
LeadingIcon={FaKeyboard}
|
LeadingIcon={FaKeyboard}
|
||||||
onClick={() => setVirtualKeyboard(!virtualKeyboard)}
|
onClick={() => setVirtualKeyboardEnabled(!isVirtualKeyboardEnabled)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -218,7 +214,7 @@ export default function Actionbar({
|
||||||
text="Extension"
|
text="Extension"
|
||||||
LeadingIcon={LuCable}
|
LeadingIcon={LuCable}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setDisableFocusTrap(true);
|
setDisableVideoFocusTrap(true);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</PopoverButton>
|
</PopoverButton>
|
||||||
|
@ -243,7 +239,7 @@ export default function Actionbar({
|
||||||
theme="light"
|
theme="light"
|
||||||
text="Virtual Keyboard"
|
text="Virtual Keyboard"
|
||||||
LeadingIcon={FaKeyboard}
|
LeadingIcon={FaKeyboard}
|
||||||
onClick={() => setVirtualKeyboard(!virtualKeyboard)}
|
onClick={() => setVirtualKeyboardEnabled(!isVirtualKeyboardEnabled)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="hidden md:block">
|
<div className="hidden md:block">
|
||||||
|
@ -268,7 +264,10 @@ export default function Actionbar({
|
||||||
theme="light"
|
theme="light"
|
||||||
text="Settings"
|
text="Settings"
|
||||||
LeadingIcon={LuSettings}
|
LeadingIcon={LuSettings}
|
||||||
onClick={() => navigateTo("/settings")}
|
onClick={() => {
|
||||||
|
setDisableVideoFocusTrap(true);
|
||||||
|
navigateTo("/settings")
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -48,7 +48,7 @@ export default function DashboardNavbar({
|
||||||
navigate("/");
|
navigate("/");
|
||||||
}, [navigate, setUser]);
|
}, [navigate, setUser]);
|
||||||
|
|
||||||
const usbState = useHidStore(state => state.usbState);
|
const { usbState } = useHidStore();
|
||||||
|
|
||||||
// for testing
|
// for testing
|
||||||
//userEmail = "user@example.org";
|
//userEmail = "user@example.org";
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { useEffect } from "react";
|
import { useEffect, useMemo } from "react";
|
||||||
|
|
||||||
import { cx } from "@/cva.config";
|
import { cx } from "@/cva.config";
|
||||||
import {
|
import {
|
||||||
|
@ -7,65 +7,68 @@ import {
|
||||||
useRTCStore,
|
useRTCStore,
|
||||||
useSettingsStore,
|
useSettingsStore,
|
||||||
useVideoStore,
|
useVideoStore,
|
||||||
|
VideoState
|
||||||
} from "@/hooks/stores";
|
} from "@/hooks/stores";
|
||||||
import { keys, modifiers } from "@/keyboardMappings";
|
import { keys, modifiers } from "@/keyboardMappings";
|
||||||
|
|
||||||
export default function InfoBar() {
|
export default function InfoBar() {
|
||||||
const activeKeys = useHidStore(state => state.activeKeys);
|
const { keysDownState } = useHidStore();
|
||||||
const activeModifiers = useHidStore(state => state.activeModifiers);
|
const { mouseX, mouseY, mouseMove } = useMouseStore();
|
||||||
const mouseX = useMouseStore(state => state.mouseX);
|
|
||||||
const mouseY = useMouseStore(state => state.mouseY);
|
|
||||||
const mouseMove = useMouseStore(state => state.mouseMove);
|
|
||||||
|
|
||||||
const videoClientSize = useVideoStore(
|
const videoClientSize = useVideoStore(
|
||||||
state => `${Math.round(state.clientWidth)}x${Math.round(state.clientHeight)}`,
|
(state: VideoState) => `${Math.round(state.clientWidth)}x${Math.round(state.clientHeight)}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
const videoSize = useVideoStore(
|
const videoSize = useVideoStore(
|
||||||
state => `${Math.round(state.width)}x${Math.round(state.height)}`,
|
(state: VideoState) => `${Math.round(state.width)}x${Math.round(state.height)}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
const rpcDataChannel = useRTCStore(state => state.rpcDataChannel);
|
const { rpcDataChannel } = useRTCStore();
|
||||||
|
const { debugMode, mouseMode, showPressedKeys } = useSettingsStore();
|
||||||
const settings = useSettingsStore();
|
|
||||||
const showPressedKeys = useSettingsStore(state => state.showPressedKeys);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!rpcDataChannel) return;
|
if (!rpcDataChannel) return;
|
||||||
rpcDataChannel.onclose = () => console.log("rpcDataChannel has closed");
|
rpcDataChannel.onclose = () => console.log("rpcDataChannel has closed");
|
||||||
rpcDataChannel.onerror = e =>
|
rpcDataChannel.onerror = (e: Event) =>
|
||||||
console.log(`Error on DataChannel '${rpcDataChannel.label}': ${e}`);
|
console.error(`Error on DataChannel '${rpcDataChannel.label}': ${e}`);
|
||||||
}, [rpcDataChannel]);
|
}, [rpcDataChannel]);
|
||||||
|
|
||||||
const keyboardLedState = useHidStore(state => state.keyboardLedState);
|
const { keyboardLedState, usbState } = useHidStore();
|
||||||
const keyboardLedStateSyncAvailable = useHidStore(state => state.keyboardLedStateSyncAvailable);
|
const { isTurnServerInUse } = useRTCStore();
|
||||||
const keyboardLedSync = useSettingsStore(state => state.keyboardLedSync);
|
const { hdmiState } = useVideoStore();
|
||||||
|
|
||||||
const isTurnServerInUse = useRTCStore(state => state.isTurnServerInUse);
|
const displayKeys = useMemo(() => {
|
||||||
|
if (!showPressedKeys)
|
||||||
|
return "";
|
||||||
|
|
||||||
const usbState = useHidStore(state => state.usbState);
|
const activeModifierMask = keysDownState.modifier || 0;
|
||||||
const hdmiState = useVideoStore(state => state.hdmiState);
|
const keysDown = keysDownState.keys || [];
|
||||||
|
const modifierNames = Object.entries(modifiers).filter(([_, mask]) => (activeModifierMask & mask) !== 0).map(([name, _]) => name);
|
||||||
|
const keyNames = Object.entries(keys).filter(([_, value]) => keysDown.includes(value)).map(([name, _]) => name);
|
||||||
|
|
||||||
|
return [...modifierNames,...keyNames].join(", ");
|
||||||
|
}, [keysDownState, showPressedKeys]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white border-t border-t-slate-800/30 text-slate-800 dark:border-t-slate-300/20 dark:bg-slate-900 dark:text-slate-300">
|
<div className="bg-white border-t border-t-slate-800/30 text-slate-800 dark:border-t-slate-300/20 dark:bg-slate-900 dark:text-slate-300">
|
||||||
<div className="flex flex-wrap items-stretch justify-between gap-1">
|
<div className="flex flex-wrap items-stretch justify-between gap-1">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<div className="flex flex-wrap items-center pl-2 gap-x-4">
|
<div className="flex flex-wrap items-center pl-2 gap-x-4">
|
||||||
{settings.debugMode ? (
|
{debugMode ? (
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<span className="text-xs font-semibold">Resolution:</span>{" "}
|
<span className="text-xs font-semibold">Resolution:</span>{" "}
|
||||||
<span className="text-xs">{videoSize}</span>
|
<span className="text-xs">{videoSize}</span>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{settings.debugMode ? (
|
{debugMode ? (
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<span className="text-xs font-semibold">Video Size: </span>
|
<span className="text-xs font-semibold">Video Size: </span>
|
||||||
<span className="text-xs">{videoClientSize}</span>
|
<span className="text-xs">{videoClientSize}</span>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{(settings.debugMode && settings.mouseMode == "absolute") ? (
|
{(debugMode && mouseMode == "absolute") ? (
|
||||||
<div className="flex w-[118px] items-center gap-x-1">
|
<div className="flex w-[118px] items-center gap-x-1">
|
||||||
<span className="text-xs font-semibold">Pointer:</span>
|
<span className="text-xs font-semibold">Pointer:</span>
|
||||||
<span className="text-xs">
|
<span className="text-xs">
|
||||||
|
@ -74,7 +77,7 @@ export default function InfoBar() {
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{(settings.debugMode && settings.mouseMode == "relative") ? (
|
{(debugMode && mouseMode == "relative") ? (
|
||||||
<div className="flex w-[118px] items-center gap-x-1">
|
<div className="flex w-[118px] items-center gap-x-1">
|
||||||
<span className="text-xs font-semibold">Last Move:</span>
|
<span className="text-xs font-semibold">Last Move:</span>
|
||||||
<span className="text-xs">
|
<span className="text-xs">
|
||||||
|
@ -85,13 +88,13 @@ export default function InfoBar() {
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{settings.debugMode && (
|
{debugMode && (
|
||||||
<div className="flex w-[156px] items-center gap-x-1">
|
<div className="flex w-[156px] items-center gap-x-1">
|
||||||
<span className="text-xs font-semibold">USB State:</span>
|
<span className="text-xs font-semibold">USB State:</span>
|
||||||
<span className="text-xs">{usbState}</span>
|
<span className="text-xs">{usbState}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{settings.debugMode && (
|
{debugMode && (
|
||||||
<div className="flex w-[156px] items-center gap-x-1">
|
<div className="flex w-[156px] items-center gap-x-1">
|
||||||
<span className="text-xs font-semibold">HDMI State:</span>
|
<span className="text-xs font-semibold">HDMI State:</span>
|
||||||
<span className="text-xs">{hdmiState}</span>
|
<span className="text-xs">{hdmiState}</span>
|
||||||
|
@ -102,14 +105,7 @@ export default function InfoBar() {
|
||||||
<div className="flex items-center gap-x-1">
|
<div className="flex items-center gap-x-1">
|
||||||
<span className="text-xs font-semibold">Keys:</span>
|
<span className="text-xs font-semibold">Keys:</span>
|
||||||
<h2 className="text-xs">
|
<h2 className="text-xs">
|
||||||
{[
|
{displayKeys}
|
||||||
...activeKeys.map(
|
|
||||||
x => Object.entries(keys).filter(y => y[1] === x)[0][0],
|
|
||||||
),
|
|
||||||
activeModifiers.map(
|
|
||||||
x => Object.entries(modifiers).filter(y => y[1] === x)[0][0],
|
|
||||||
),
|
|
||||||
].join(", ")}
|
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -122,23 +118,10 @@ export default function InfoBar() {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{keyboardLedStateSyncAvailable ? (
|
|
||||||
<div
|
<div
|
||||||
className={cx(
|
className={cx(
|
||||||
"shrink-0 p-1 px-1.5 text-xs",
|
"shrink-0 p-1 px-1.5 text-xs",
|
||||||
keyboardLedSync !== "browser"
|
keyboardLedState.caps_lock
|
||||||
? "text-black dark:text-white"
|
|
||||||
: "text-slate-800/20 dark:text-slate-300/20",
|
|
||||||
)}
|
|
||||||
title={"Your keyboard LED state is managed by" + (keyboardLedSync === "browser" ? " the browser" : " the host")}
|
|
||||||
>
|
|
||||||
{keyboardLedSync === "browser" ? "Browser" : "Host"}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
<div
|
|
||||||
className={cx(
|
|
||||||
"shrink-0 p-1 px-1.5 text-xs",
|
|
||||||
keyboardLedState?.caps_lock
|
|
||||||
? "text-black dark:text-white"
|
? "text-black dark:text-white"
|
||||||
: "text-slate-800/20 dark:text-slate-300/20",
|
: "text-slate-800/20 dark:text-slate-300/20",
|
||||||
)}
|
)}
|
||||||
|
@ -148,7 +131,7 @@ export default function InfoBar() {
|
||||||
<div
|
<div
|
||||||
className={cx(
|
className={cx(
|
||||||
"shrink-0 p-1 px-1.5 text-xs",
|
"shrink-0 p-1 px-1.5 text-xs",
|
||||||
keyboardLedState?.num_lock
|
keyboardLedState.num_lock
|
||||||
? "text-black dark:text-white"
|
? "text-black dark:text-white"
|
||||||
: "text-slate-800/20 dark:text-slate-300/20",
|
: "text-slate-800/20 dark:text-slate-300/20",
|
||||||
)}
|
)}
|
||||||
|
@ -158,23 +141,28 @@ export default function InfoBar() {
|
||||||
<div
|
<div
|
||||||
className={cx(
|
className={cx(
|
||||||
"shrink-0 p-1 px-1.5 text-xs",
|
"shrink-0 p-1 px-1.5 text-xs",
|
||||||
keyboardLedState?.scroll_lock
|
keyboardLedState.scroll_lock
|
||||||
? "text-black dark:text-white"
|
? "text-black dark:text-white"
|
||||||
: "text-slate-800/20 dark:text-slate-300/20",
|
: "text-slate-800/20 dark:text-slate-300/20",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
Scroll Lock
|
Scroll Lock
|
||||||
</div>
|
</div>
|
||||||
{keyboardLedState?.compose ? (
|
{keyboardLedState.compose ? (
|
||||||
<div className="shrink-0 p-1 px-1.5 text-xs">
|
<div className="shrink-0 p-1 px-1.5 text-xs">
|
||||||
Compose
|
Compose
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{keyboardLedState?.kana ? (
|
{keyboardLedState.kana ? (
|
||||||
<div className="shrink-0 p-1 px-1.5 text-xs">
|
<div className="shrink-0 p-1 px-1.5 text-xs">
|
||||||
Kana
|
Kana
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
{keyboardLedState.shift ? (
|
||||||
|
<div className="shrink-0 p-1 px-1.5 text-xs">
|
||||||
|
Shift
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||||
export default function MacroBar() {
|
export default function MacroBar() {
|
||||||
const { macros, initialized, loadMacros, setSendFn } = useMacrosStore();
|
const { macros, initialized, loadMacros, setSendFn } = useMacrosStore();
|
||||||
const { executeMacro } = useKeyboard();
|
const { executeMacro } = useKeyboard();
|
||||||
const [send] = useJsonRpc();
|
const { send } = useJsonRpc();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSendFn(send);
|
setSendFn(send);
|
||||||
|
|
|
@ -1,17 +1,18 @@
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { LuPlus } from "react-icons/lu";
|
import { LuPlus } from "react-icons/lu";
|
||||||
|
|
||||||
import { KeySequence } from "@/hooks/stores";
|
|
||||||
import { Button } from "@/components/Button";
|
import { Button } from "@/components/Button";
|
||||||
import { InputFieldWithLabel, FieldError } from "@/components/InputField";
|
import FieldLabel from "@/components/FieldLabel";
|
||||||
import Fieldset from "@/components/Fieldset";
|
import Fieldset from "@/components/Fieldset";
|
||||||
|
import { InputFieldWithLabel, FieldError } from "@/components/InputField";
|
||||||
import { MacroStepCard } from "@/components/MacroStepCard";
|
import { MacroStepCard } from "@/components/MacroStepCard";
|
||||||
import {
|
import {
|
||||||
DEFAULT_DELAY,
|
DEFAULT_DELAY,
|
||||||
MAX_STEPS_PER_MACRO,
|
MAX_STEPS_PER_MACRO,
|
||||||
MAX_KEYS_PER_STEP,
|
MAX_KEYS_PER_STEP,
|
||||||
} from "@/constants/macros";
|
} from "@/constants/macros";
|
||||||
import FieldLabel from "@/components/FieldLabel";
|
import { KeySequence } from "@/hooks/stores";
|
||||||
|
import useKeyboardLayout from "@/hooks/useKeyboardLayout";
|
||||||
|
|
||||||
interface ValidationErrors {
|
interface ValidationErrors {
|
||||||
name?: string;
|
name?: string;
|
||||||
|
@ -44,6 +45,7 @@ export function MacroForm({
|
||||||
const [keyQueries, setKeyQueries] = useState<Record<number, string>>({});
|
const [keyQueries, setKeyQueries] = useState<Record<number, string>>({});
|
||||||
const [errors, setErrors] = useState<ValidationErrors>({});
|
const [errors, setErrors] = useState<ValidationErrors>({});
|
||||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||||
|
const { selectedKeyboard } = useKeyboardLayout();
|
||||||
|
|
||||||
const showTemporaryError = (message: string) => {
|
const showTemporaryError = (message: string) => {
|
||||||
setErrorMessage(message);
|
setErrorMessage(message);
|
||||||
|
@ -234,6 +236,7 @@ export function MacroForm({
|
||||||
}
|
}
|
||||||
onDelayChange={delay => handleDelayChange(stepIndex, delay)}
|
onDelayChange={delay => handleDelayChange(stepIndex, delay)}
|
||||||
isLastStep={stepIndex === (macro.steps?.length || 0) - 1}
|
isLastStep={stepIndex === (macro.steps?.length || 0) - 1}
|
||||||
|
keyboard={selectedKeyboard}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,23 +1,18 @@
|
||||||
|
import { useMemo } from "react";
|
||||||
import { LuArrowUp, LuArrowDown, LuX, LuTrash2 } from "react-icons/lu";
|
import { LuArrowUp, LuArrowDown, LuX, LuTrash2 } from "react-icons/lu";
|
||||||
|
|
||||||
import { Button } from "@/components/Button";
|
import { Button } from "@/components/Button";
|
||||||
import { Combobox } from "@/components/Combobox";
|
import { Combobox } from "@/components/Combobox";
|
||||||
import { SelectMenuBasic } from "@/components/SelectMenuBasic";
|
import { SelectMenuBasic } from "@/components/SelectMenuBasic";
|
||||||
import Card from "@/components/Card";
|
import Card from "@/components/Card";
|
||||||
import { keys, modifiers, keyDisplayMap } from "@/keyboardMappings";
|
|
||||||
import { MAX_KEYS_PER_STEP, DEFAULT_DELAY } from "@/constants/macros";
|
|
||||||
import FieldLabel from "@/components/FieldLabel";
|
import FieldLabel from "@/components/FieldLabel";
|
||||||
|
import { MAX_KEYS_PER_STEP, DEFAULT_DELAY } from "@/constants/macros";
|
||||||
|
import { KeyboardLayout } from "@/keyboardLayouts";
|
||||||
|
import { keys, modifiers } from "@/keyboardMappings";
|
||||||
|
|
||||||
// Filter out modifier keys since they're handled in the modifiers section
|
// Filter out modifier keys since they're handled in the modifiers section
|
||||||
const modifierKeyPrefixes = ['Alt', 'Control', 'Shift', 'Meta'];
|
const modifierKeyPrefixes = ['Alt', 'Control', 'Shift', 'Meta'];
|
||||||
|
|
||||||
const keyOptions = Object.keys(keys)
|
|
||||||
.filter(key => !modifierKeyPrefixes.some(prefix => key.startsWith(prefix)))
|
|
||||||
.map(key => ({
|
|
||||||
value: key,
|
|
||||||
label: keyDisplayMap[key] || key,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const modifierOptions = Object.keys(modifiers).map(modifier => ({
|
const modifierOptions = Object.keys(modifiers).map(modifier => ({
|
||||||
value: modifier,
|
value: modifier,
|
||||||
label: modifier.replace(/^(Control|Alt|Shift|Meta)(Left|Right)$/, "$1 $2"),
|
label: modifier.replace(/^(Control|Alt|Shift|Meta)(Left|Right)$/, "$1 $2"),
|
||||||
|
@ -67,6 +62,7 @@ interface MacroStepCardProps {
|
||||||
onModifierChange: (modifiers: string[]) => void;
|
onModifierChange: (modifiers: string[]) => void;
|
||||||
onDelayChange: (delay: number) => void;
|
onDelayChange: (delay: number) => void;
|
||||||
isLastStep: boolean;
|
isLastStep: boolean;
|
||||||
|
keyboard: KeyboardLayout
|
||||||
}
|
}
|
||||||
|
|
||||||
const ensureArray = <T,>(arr: T[] | null | undefined): T[] => {
|
const ensureArray = <T,>(arr: T[] | null | undefined): T[] => {
|
||||||
|
@ -84,9 +80,22 @@ export function MacroStepCard({
|
||||||
keyQuery,
|
keyQuery,
|
||||||
onModifierChange,
|
onModifierChange,
|
||||||
onDelayChange,
|
onDelayChange,
|
||||||
isLastStep
|
isLastStep,
|
||||||
|
keyboard
|
||||||
}: MacroStepCardProps) {
|
}: MacroStepCardProps) {
|
||||||
const getFilteredKeys = () => {
|
const { keyDisplayMap } = keyboard;
|
||||||
|
|
||||||
|
const keyOptions = useMemo(() =>
|
||||||
|
Object.keys(keys)
|
||||||
|
.filter(key => !modifierKeyPrefixes.some(prefix => key.startsWith(prefix)))
|
||||||
|
.map(key => ({
|
||||||
|
value: key,
|
||||||
|
label: keyDisplayMap[key] || key,
|
||||||
|
})),
|
||||||
|
[keyDisplayMap]
|
||||||
|
);
|
||||||
|
|
||||||
|
const filteredKeys = useMemo(() => {
|
||||||
const selectedKeys = ensureArray(step.keys);
|
const selectedKeys = ensureArray(step.keys);
|
||||||
const availableKeys = keyOptions.filter(option => !selectedKeys.includes(option.value));
|
const availableKeys = keyOptions.filter(option => !selectedKeys.includes(option.value));
|
||||||
|
|
||||||
|
@ -95,7 +104,7 @@ export function MacroStepCard({
|
||||||
} else {
|
} else {
|
||||||
return availableKeys.filter(option => option.label.toLowerCase().includes(keyQuery.toLowerCase()));
|
return availableKeys.filter(option => option.label.toLowerCase().includes(keyQuery.toLowerCase()));
|
||||||
}
|
}
|
||||||
};
|
}, [keyOptions, keyQuery, step.keys]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="p-4">
|
<Card className="p-4">
|
||||||
|
@ -204,7 +213,7 @@ export function MacroStepCard({
|
||||||
}}
|
}}
|
||||||
displayValue={() => keyQuery}
|
displayValue={() => keyQuery}
|
||||||
onInputChange={onKeyQueryChange}
|
onInputChange={onKeyQueryChange}
|
||||||
options={getFilteredKeys}
|
options={() => filteredKeys}
|
||||||
disabledMessage="Max keys reached"
|
disabledMessage="Max keys reached"
|
||||||
size="SM"
|
size="SM"
|
||||||
immediate
|
immediate
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import "react-simple-keyboard/build/css/index.css";
|
import "react-simple-keyboard/build/css/index.css";
|
||||||
import { ChevronDownIcon } from "@heroicons/react/16/solid";
|
import { ChevronDownIcon } from "@heroicons/react/16/solid";
|
||||||
import { useEffect } from "react";
|
import { useEffect, useMemo } from "react";
|
||||||
import { useXTerm } from "react-xtermjs";
|
import { useXTerm } from "react-xtermjs";
|
||||||
import { FitAddon } from "@xterm/addon-fit";
|
import { FitAddon } from "@xterm/addon-fit";
|
||||||
import { WebLinksAddon } from "@xterm/addon-web-links";
|
import { WebLinksAddon } from "@xterm/addon-web-links";
|
||||||
|
@ -65,21 +65,22 @@ function Terminal({
|
||||||
readonly dataChannel: RTCDataChannel;
|
readonly dataChannel: RTCDataChannel;
|
||||||
readonly type: AvailableTerminalTypes;
|
readonly type: AvailableTerminalTypes;
|
||||||
}) {
|
}) {
|
||||||
const enableTerminal = useUiStore(state => state.terminalType == type);
|
const { terminalType, setTerminalType, setDisableVideoFocusTrap } = useUiStore();
|
||||||
const setTerminalType = useUiStore(state => state.setTerminalType);
|
|
||||||
const setDisableVideoFocusTrap = useUiStore(state => state.setDisableVideoFocusTrap);
|
|
||||||
|
|
||||||
const { instance, ref } = useXTerm({ options: TERMINAL_CONFIG });
|
const { instance, ref } = useXTerm({ options: TERMINAL_CONFIG });
|
||||||
|
|
||||||
|
const isTerminalTypeEnabled = useMemo(() => {
|
||||||
|
return terminalType == type;
|
||||||
|
}, [terminalType, type]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setDisableVideoFocusTrap(enableTerminal);
|
setDisableVideoFocusTrap(isTerminalTypeEnabled);
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
setDisableVideoFocusTrap(false);
|
setDisableVideoFocusTrap(false);
|
||||||
};
|
};
|
||||||
}, [enableTerminal, setDisableVideoFocusTrap]);
|
}, [setDisableVideoFocusTrap, isTerminalTypeEnabled]);
|
||||||
|
|
||||||
const readyState = dataChannel.readyState;
|
const readyState = dataChannel.readyState;
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -175,9 +176,9 @@ function Terminal({
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
"pointer-events-none translate-y-[500px] opacity-100 transition duration-300":
|
"pointer-events-none translate-y-[500px] opacity-100 transition duration-300":
|
||||||
!enableTerminal,
|
!isTerminalTypeEnabled,
|
||||||
"pointer-events-auto -translate-y-[0px] opacity-100 transition duration-300":
|
"pointer-events-auto -translate-y-[0px] opacity-100 transition duration-300":
|
||||||
enableTerminal,
|
isTerminalTypeEnabled,
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|
|
@ -4,9 +4,7 @@ import { cx } from "@/cva.config";
|
||||||
import KeyboardAndMouseConnectedIcon from "@/assets/keyboard-and-mouse-connected.png";
|
import KeyboardAndMouseConnectedIcon from "@/assets/keyboard-and-mouse-connected.png";
|
||||||
import LoadingSpinner from "@components/LoadingSpinner";
|
import LoadingSpinner from "@components/LoadingSpinner";
|
||||||
import StatusCard from "@components/StatusCards";
|
import StatusCard from "@components/StatusCards";
|
||||||
import { HidState } from "@/hooks/stores";
|
import { USBStates } from "@/hooks/stores";
|
||||||
|
|
||||||
type USBStates = HidState["usbState"];
|
|
||||||
|
|
||||||
type StatusProps = Record<
|
type StatusProps = Record<
|
||||||
USBStates,
|
USBStates,
|
||||||
|
@ -67,7 +65,7 @@ export default function USBStateStatus({
|
||||||
};
|
};
|
||||||
const props = StatusCardProps[state];
|
const props = StatusCardProps[state];
|
||||||
if (!props) {
|
if (!props) {
|
||||||
console.log("Unsupported USB state: ", state);
|
console.warn("Unsupported USB state: ", state);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -59,7 +59,7 @@ const usbPresets = [
|
||||||
];
|
];
|
||||||
|
|
||||||
export function UsbDeviceSetting() {
|
export function UsbDeviceSetting() {
|
||||||
const [send] = useJsonRpc();
|
const { send } = useJsonRpc();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const [usbDeviceConfig, setUsbDeviceConfig] =
|
const [usbDeviceConfig, setUsbDeviceConfig] =
|
||||||
|
|
|
@ -54,7 +54,7 @@ const usbConfigs = [
|
||||||
type UsbConfigMap = Record<string, USBConfig>;
|
type UsbConfigMap = Record<string, USBConfig>;
|
||||||
|
|
||||||
export function UsbInfoSetting() {
|
export function UsbInfoSetting() {
|
||||||
const [send] = useJsonRpc();
|
const { send } = useJsonRpc();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const [usbConfigProduct, setUsbConfigProduct] = useState("");
|
const [usbConfigProduct, setUsbConfigProduct] = useState("");
|
||||||
|
@ -101,8 +101,8 @@ export function UsbInfoSetting() {
|
||||||
`Failed to load USB Config: ${resp.error.data || "Unknown error"}`,
|
`Failed to load USB Config: ${resp.error.data || "Unknown error"}`,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
console.log("syncUsbConfigProduct#getUsbConfig result:", resp.result);
|
|
||||||
const usbConfigState = resp.result as UsbConfigState;
|
const usbConfigState = resp.result as UsbConfigState;
|
||||||
|
console.log("syncUsbConfigProduct#getUsbConfig result:", usbConfigState);
|
||||||
const product = usbConfigs.map(u => u.value).includes(usbConfigState.product)
|
const product = usbConfigs.map(u => u.value).includes(usbConfigState.product)
|
||||||
? usbConfigState.product
|
? usbConfigState.product
|
||||||
: "custom";
|
: "custom";
|
||||||
|
@ -205,7 +205,7 @@ function USBConfigDialog({
|
||||||
product: "",
|
product: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
const [send] = useJsonRpc();
|
const { send } = useJsonRpc();
|
||||||
|
|
||||||
const syncUsbConfig = useCallback(() => {
|
const syncUsbConfig = useCallback(() => {
|
||||||
send("getUsbConfig", {}, resp => {
|
send("getUsbConfig", {}, resp => {
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { useShallow } from "zustand/react/shallow";
|
|
||||||
import { ChevronDownIcon } from "@heroicons/react/16/solid";
|
import { ChevronDownIcon } from "@heroicons/react/16/solid";
|
||||||
import { AnimatePresence, motion } from "framer-motion";
|
import { AnimatePresence, motion } from "framer-motion";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
@ -13,9 +12,10 @@ import "react-simple-keyboard/build/css/index.css";
|
||||||
import AttachIconRaw from "@/assets/attach-icon.svg";
|
import AttachIconRaw from "@/assets/attach-icon.svg";
|
||||||
import DetachIconRaw from "@/assets/detach-icon.svg";
|
import DetachIconRaw from "@/assets/detach-icon.svg";
|
||||||
import { cx } from "@/cva.config";
|
import { cx } from "@/cva.config";
|
||||||
import { useHidStore, useSettingsStore, useUiStore } from "@/hooks/stores";
|
import { useHidStore, useUiStore } from "@/hooks/stores";
|
||||||
import useKeyboard from "@/hooks/useKeyboard";
|
import useKeyboard from "@/hooks/useKeyboard";
|
||||||
import { keyDisplayMap, keys, modifiers } from "@/keyboardMappings";
|
import useKeyboardLayout from "@/hooks/useKeyboardLayout";
|
||||||
|
import { keys, modifiers, latchingKeys, decodeModifiers } from "@/keyboardMappings";
|
||||||
|
|
||||||
export const DetachIcon = ({ className }: { className?: string }) => {
|
export const DetachIcon = ({ className }: { className?: string }) => {
|
||||||
return <img src={DetachIconRaw} alt="Detach Icon" className={className} />;
|
return <img src={DetachIconRaw} alt="Detach Icon" className={className} />;
|
||||||
|
@ -26,33 +26,46 @@ const AttachIcon = ({ className }: { className?: string }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
function KeyboardWrapper() {
|
function KeyboardWrapper() {
|
||||||
const [layoutName, setLayoutName] = useState("default");
|
|
||||||
|
|
||||||
const keyboardRef = useRef<HTMLDivElement>(null);
|
const keyboardRef = useRef<HTMLDivElement>(null);
|
||||||
const showAttachedVirtualKeyboard = useUiStore(
|
const { isAttachedVirtualKeyboardVisible, setAttachedVirtualKeyboardVisibility } = useUiStore();
|
||||||
state => state.isAttachedVirtualKeyboardVisible,
|
const { keysDownState, /* keyboardLedState,*/ isVirtualKeyboardEnabled, setVirtualKeyboardEnabled } = useHidStore();
|
||||||
);
|
const { handleKeyPress, executeMacro } = useKeyboard();
|
||||||
const setShowAttachedVirtualKeyboard = useUiStore(
|
const { selectedKeyboard } = useKeyboardLayout();
|
||||||
state => state.setAttachedVirtualKeyboardVisibility,
|
|
||||||
);
|
|
||||||
|
|
||||||
const { sendKeyboardEvent, resetKeyboardState } = useKeyboard();
|
|
||||||
|
|
||||||
const [isDragging, setIsDragging] = useState(false);
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
const [position, setPosition] = useState({ x: 0, y: 0 });
|
const [position, setPosition] = useState({ x: 0, y: 0 });
|
||||||
const [newPosition, setNewPosition] = useState({ x: 0, y: 0 });
|
const [newPosition, setNewPosition] = useState({ x: 0, y: 0 });
|
||||||
|
|
||||||
const isCapsLockActive = useHidStore(useShallow(state => state.keyboardLedState?.caps_lock));
|
const keyDisplayMap = useMemo(() => {
|
||||||
|
return selectedKeyboard.keyDisplayMap;
|
||||||
|
}, [selectedKeyboard]);
|
||||||
|
|
||||||
// HID related states
|
const virtualKeyboard = useMemo(() => {
|
||||||
const keyboardLedStateSyncAvailable = useHidStore(state => state.keyboardLedStateSyncAvailable);
|
return selectedKeyboard.virtualKeyboard;
|
||||||
const keyboardLedSync = useSettingsStore(state => state.keyboardLedSync);
|
}, [selectedKeyboard]);
|
||||||
const isKeyboardLedManagedByHost = useMemo(() =>
|
|
||||||
keyboardLedSync !== "browser" && keyboardLedStateSyncAvailable,
|
|
||||||
[keyboardLedSync, keyboardLedStateSyncAvailable],
|
|
||||||
);
|
|
||||||
|
|
||||||
const setIsCapsLockActive = useHidStore(state => state.setIsCapsLockActive);
|
//const isCapsLockActive = useMemo(() => {
|
||||||
|
// return (keyboardLedState.caps_lock);
|
||||||
|
//}, [keyboardLedState]);
|
||||||
|
|
||||||
|
const { isShiftActive, /*isControlActive, isAltActive, isMetaActive, isAltGrActive*/ } = useMemo(() => {
|
||||||
|
return decodeModifiers(keysDownState.modifier);
|
||||||
|
}, [keysDownState]);
|
||||||
|
|
||||||
|
const mainLayoutName = useMemo(() => {
|
||||||
|
const layoutName = isShiftActive ? "shift": "default";
|
||||||
|
return layoutName;
|
||||||
|
}, [isShiftActive]);
|
||||||
|
|
||||||
|
const keyNamesForDownKeys = useMemo(() => {
|
||||||
|
const activeModifierMask = keysDownState.modifier || 0;
|
||||||
|
const modifierNames = Object.entries(modifiers).filter(([_, mask]) => (activeModifierMask & mask) !== 0).map(([name, _]) => name);
|
||||||
|
|
||||||
|
const keysDown = keysDownState.keys || [];
|
||||||
|
const keyNames = Object.entries(keys).filter(([_, value]) => keysDown.includes(value)).map(([name, _]) => name);
|
||||||
|
|
||||||
|
return [...modifierNames,...keyNames, ' ']; // we have to have at least one space to avoid keyboard whining
|
||||||
|
}, [keysDownState]);
|
||||||
|
|
||||||
const startDrag = useCallback((e: MouseEvent | TouchEvent) => {
|
const startDrag = useCallback((e: MouseEvent | TouchEvent) => {
|
||||||
if (!keyboardRef.current) return;
|
if (!keyboardRef.current) return;
|
||||||
|
@ -123,94 +136,69 @@ function KeyboardWrapper() {
|
||||||
};
|
};
|
||||||
}, [endDrag, onDrag, startDrag]);
|
}, [endDrag, onDrag, startDrag]);
|
||||||
|
|
||||||
const onKeyDown = useCallback(
|
const onKeyUp = useCallback(
|
||||||
(key: string) => {
|
async (_: string, e: MouseEvent | undefined) => {
|
||||||
const isKeyShift = key === "{shift}" || key === "ShiftLeft" || key === "ShiftRight";
|
e?.preventDefault();
|
||||||
const isKeyCaps = key === "CapsLock";
|
e?.stopPropagation();
|
||||||
const cleanKey = key.replace(/[()]/g, "");
|
},
|
||||||
const keyHasShiftModifier = key.includes("(");
|
[]
|
||||||
|
|
||||||
// Handle toggle of layout for shift or caps lock
|
|
||||||
const toggleLayout = () => {
|
|
||||||
setLayoutName(prevLayout => (prevLayout === "default" ? "shift" : "default"));
|
|
||||||
};
|
|
||||||
|
|
||||||
if (key === "CtrlAltDelete") {
|
|
||||||
sendKeyboardEvent(
|
|
||||||
[keys["Delete"]],
|
|
||||||
[modifiers["ControlLeft"], modifiers["AltLeft"]],
|
|
||||||
);
|
);
|
||||||
setTimeout(resetKeyboardState, 100);
|
|
||||||
|
const onKeyDown = useCallback(
|
||||||
|
async (key: string, e: MouseEvent | undefined) => {
|
||||||
|
e?.preventDefault();
|
||||||
|
e?.stopPropagation();
|
||||||
|
|
||||||
|
// handle the fake key-macros we have defined for common combinations
|
||||||
|
if (key === "CtrlAltDelete") {
|
||||||
|
await executeMacro([ { keys: ["Delete"], modifiers: ["ControlLeft", "AltLeft"], delay: 100 } ]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (key === "AltMetaEscape") {
|
if (key === "AltMetaEscape") {
|
||||||
sendKeyboardEvent(
|
await executeMacro([ { keys: ["Escape"], modifiers: ["AltLeft", "MetaLeft"], delay: 100 } ]);
|
||||||
[keys["Escape"]],
|
|
||||||
[modifiers["MetaLeft"], modifiers["AltLeft"]],
|
|
||||||
);
|
|
||||||
|
|
||||||
setTimeout(resetKeyboardState, 100);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (key === "CtrlAltBackspace") {
|
if (key === "CtrlAltBackspace") {
|
||||||
sendKeyboardEvent(
|
await executeMacro([ { keys: ["Backspace"], modifiers: ["ControlLeft", "AltLeft"], delay: 100 } ]);
|
||||||
[keys["Backspace"]],
|
|
||||||
[modifiers["ControlLeft"], modifiers["AltLeft"]],
|
|
||||||
);
|
|
||||||
|
|
||||||
setTimeout(resetKeyboardState, 100);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isKeyShift || isKeyCaps) {
|
// if they press any of the latching keys, we send a keypress down event and the release it automatically (on timer)
|
||||||
toggleLayout();
|
if (latchingKeys.includes(key)) {
|
||||||
|
console.debug(`Latching key pressed: ${key} sending down and delayed up pair`);
|
||||||
if (isCapsLockActive) {
|
handleKeyPress(keys[key], true)
|
||||||
if (!isKeyboardLedManagedByHost) {
|
setTimeout(() => handleKeyPress(keys[key], false), 100);
|
||||||
setIsCapsLockActive(false);
|
|
||||||
}
|
|
||||||
sendKeyboardEvent([keys["CapsLock"]], []);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if they press any of the dynamic keys, we send a keypress down event but we don't release it until they click it again
|
||||||
|
if (Object.keys(modifiers).includes(key)) {
|
||||||
|
const currentlyDown = keyNamesForDownKeys.includes(key);
|
||||||
|
console.debug(`Dynamic key pressed: ${key} was currently down: ${currentlyDown}, toggling state`);
|
||||||
|
handleKeyPress(keys[key], !currentlyDown)
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle caps lock state change
|
// otherwise, just treat it as a down+up pair
|
||||||
if (isKeyCaps && !isKeyboardLedManagedByHost) {
|
const cleanKey = key.replace(/[()]/g, "");
|
||||||
setIsCapsLockActive(!isCapsLockActive);
|
console.debug(`Regular key pressed: ${cleanKey} sending down and up pair`);
|
||||||
}
|
handleKeyPress(keys[cleanKey], true);
|
||||||
|
setTimeout(() => handleKeyPress(keys[cleanKey], false), 50);
|
||||||
// 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);
|
|
||||||
},
|
},
|
||||||
[isCapsLockActive, isKeyboardLedManagedByHost, sendKeyboardEvent, resetKeyboardState, setIsCapsLockActive],
|
[executeMacro, handleKeyPress, keyNamesForDownKeys],
|
||||||
);
|
);
|
||||||
|
|
||||||
const virtualKeyboard = useHidStore(state => state.isVirtualKeyboardEnabled);
|
|
||||||
const setVirtualKeyboard = useHidStore(state => state.setVirtualKeyboardEnabled);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="transition-all duration-500 ease-in-out"
|
className="transition-all duration-500 ease-in-out"
|
||||||
style={{
|
style={{
|
||||||
marginBottom: virtualKeyboard ? "0px" : `-${350}px`,
|
marginBottom: isVirtualKeyboardEnabled ? "0px" : `-${350}px`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{virtualKeyboard && (
|
{isVirtualKeyboardEnabled && (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: "100%" }}
|
initial={{ opacity: 0, y: "100%" }}
|
||||||
animate={{ opacity: 1, y: "0%" }}
|
animate={{ opacity: 1, y: "0%" }}
|
||||||
|
@ -222,30 +210,30 @@ function KeyboardWrapper() {
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={cx(
|
className={cx(
|
||||||
!showAttachedVirtualKeyboard
|
!isAttachedVirtualKeyboardVisible
|
||||||
? "fixed left-0 top-0 z-50 select-none"
|
? "fixed left-0 top-0 z-50 select-none"
|
||||||
: "relative",
|
: "relative",
|
||||||
)}
|
)}
|
||||||
ref={keyboardRef}
|
ref={keyboardRef}
|
||||||
style={{
|
style={{
|
||||||
...(!showAttachedVirtualKeyboard
|
...(!isAttachedVirtualKeyboardVisible
|
||||||
? { transform: `translate(${newPosition.x}px, ${newPosition.y}px)` }
|
? { transform: `translate(${newPosition.x}px, ${newPosition.y}px)` }
|
||||||
: {}),
|
: {}),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Card
|
<Card
|
||||||
className={cx("overflow-hidden", {
|
className={cx("overflow-hidden", {
|
||||||
"rounded-none": showAttachedVirtualKeyboard,
|
"rounded-none": isAttachedVirtualKeyboardVisible,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-center border-b border-b-slate-800/30 bg-white px-2 py-1 dark:border-b-slate-300/20 dark:bg-slate-800">
|
<div className="flex items-center justify-center border-b border-b-slate-800/30 bg-white px-2 py-1 dark:border-b-slate-300/20 dark:bg-slate-800">
|
||||||
<div className="absolute left-2 flex items-center gap-x-2">
|
<div className="absolute left-2 flex items-center gap-x-2">
|
||||||
{showAttachedVirtualKeyboard ? (
|
{isAttachedVirtualKeyboardVisible ? (
|
||||||
<Button
|
<Button
|
||||||
size="XS"
|
size="XS"
|
||||||
theme="light"
|
theme="light"
|
||||||
text="Detach"
|
text="Detach"
|
||||||
onClick={() => setShowAttachedVirtualKeyboard(false)}
|
onClick={() => setAttachedVirtualKeyboardVisibility(false)}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
|
@ -253,7 +241,7 @@ function KeyboardWrapper() {
|
||||||
theme="light"
|
theme="light"
|
||||||
text="Attach"
|
text="Attach"
|
||||||
LeadingIcon={AttachIcon}
|
LeadingIcon={AttachIcon}
|
||||||
onClick={() => setShowAttachedVirtualKeyboard(true)}
|
onClick={() => setAttachedVirtualKeyboardVisibility(true)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -266,7 +254,7 @@ function KeyboardWrapper() {
|
||||||
theme="light"
|
theme="light"
|
||||||
text="Hide"
|
text="Hide"
|
||||||
LeadingIcon={ChevronDownIcon}
|
LeadingIcon={ChevronDownIcon}
|
||||||
onClick={() => setVirtualKeyboard(false)}
|
onClick={() => setVirtualKeyboardEnabled(false)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -275,66 +263,73 @@ function KeyboardWrapper() {
|
||||||
<div className="flex flex-col bg-blue-50/80 md:flex-row dark:bg-slate-700">
|
<div className="flex flex-col bg-blue-50/80 md:flex-row dark:bg-slate-700">
|
||||||
<Keyboard
|
<Keyboard
|
||||||
baseClass="simple-keyboard-main"
|
baseClass="simple-keyboard-main"
|
||||||
layoutName={layoutName}
|
layoutName={mainLayoutName}
|
||||||
onKeyPress={onKeyDown}
|
onKeyPress={onKeyDown}
|
||||||
|
onKeyReleased={onKeyUp}
|
||||||
buttonTheme={[
|
buttonTheme={[
|
||||||
{
|
{
|
||||||
class: "combination-key",
|
class: "combination-key",
|
||||||
buttons: "CtrlAltDelete AltMetaEscape CtrlAltBackspace",
|
buttons: "CtrlAltDelete AltMetaEscape CtrlAltBackspace",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
class: "down-key",
|
||||||
|
buttons: keyNamesForDownKeys.join(" "),
|
||||||
|
},
|
||||||
]}
|
]}
|
||||||
display={keyDisplayMap}
|
display={keyDisplayMap}
|
||||||
layout={{
|
layout={virtualKeyboard.main}
|
||||||
default: [
|
|
||||||
"CtrlAltDelete AltMetaEscape CtrlAltBackspace",
|
|
||||||
"Escape F1 F2 F3 F4 F5 F6 F7 F8 F9 F10 F11 F12",
|
|
||||||
"Backquote Digit1 Digit2 Digit3 Digit4 Digit5 Digit6 Digit7 Digit8 Digit9 Digit0 Minus Equal Backspace",
|
|
||||||
"Tab KeyQ KeyW KeyE KeyR KeyT KeyY KeyU KeyI KeyO KeyP BracketLeft BracketRight Backslash",
|
|
||||||
"CapsLock KeyA KeyS KeyD KeyF KeyG KeyH KeyJ KeyK KeyL Semicolon Quote Enter",
|
|
||||||
"ShiftLeft KeyZ KeyX KeyC KeyV KeyB KeyN KeyM Comma Period Slash ShiftRight",
|
|
||||||
"ControlLeft AltLeft MetaLeft Space MetaRight AltRight",
|
|
||||||
],
|
|
||||||
shift: [
|
|
||||||
"CtrlAltDelete AltMetaEscape CtrlAltBackspace",
|
|
||||||
"Escape F1 F2 F3 F4 F5 F6 F7 F8 F9 F10 F11 F12",
|
|
||||||
"(Backquote) (Digit1) (Digit2) (Digit3) (Digit4) (Digit5) (Digit6) (Digit7) (Digit8) (Digit9) (Digit0) (Minus) (Equal) (Backspace)",
|
|
||||||
"Tab (KeyQ) (KeyW) (KeyE) (KeyR) (KeyT) (KeyY) (KeyU) (KeyI) (KeyO) (KeyP) (BracketLeft) (BracketRight) (Backslash)",
|
|
||||||
"CapsLock (KeyA) (KeyS) (KeyD) (KeyF) (KeyG) (KeyH) (KeyJ) (KeyK) (KeyL) (Semicolon) (Quote) Enter",
|
|
||||||
"ShiftLeft (KeyZ) (KeyX) (KeyC) (KeyV) (KeyB) (KeyN) (KeyM) (Comma) (Period) (Slash) ShiftRight",
|
|
||||||
"ControlLeft AltLeft MetaLeft Space MetaRight AltRight",
|
|
||||||
],
|
|
||||||
}}
|
|
||||||
disableButtonHold={true}
|
disableButtonHold={true}
|
||||||
syncInstanceInputs={true}
|
enableLayoutCandidates={false}
|
||||||
debug={false}
|
preventMouseDownDefault={true}
|
||||||
|
preventMouseUpDefault={true}
|
||||||
|
stopMouseDownPropagation={true}
|
||||||
|
stopMouseUpPropagation={true}
|
||||||
|
physicalKeyboardHighlight={true}
|
||||||
|
physicalKeyboardHighlightPreventDefault={true}
|
||||||
|
physicalKeyboardHighlightTextColor="black"
|
||||||
|
physicalKeyboardHighlightBgColor="lightblue"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="controlArrows">
|
<div className="controlArrows">
|
||||||
<Keyboard
|
<Keyboard
|
||||||
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="default"
|
||||||
onKeyPress={onKeyDown}
|
onKeyPress={onKeyDown}
|
||||||
|
onKeyReleased={onKeyUp}
|
||||||
display={keyDisplayMap}
|
display={keyDisplayMap}
|
||||||
layout={{
|
layout={virtualKeyboard.control}
|
||||||
default: ["PrintScreen ScrollLock Pause", "Insert Home Pageup", "Delete End Pagedown"],
|
disableButtonHold={true}
|
||||||
shift: ["(PrintScreen) ScrollLock (Pause)", "Insert Home Pageup", "Delete End Pagedown"],
|
enableLayoutCandidates={false}
|
||||||
}}
|
preventMouseDownDefault={true}
|
||||||
syncInstanceInputs={true}
|
preventMouseUpDefault={true}
|
||||||
debug={false}
|
stopMouseDownPropagation={true}
|
||||||
|
stopMouseUpPropagation={true}
|
||||||
|
physicalKeyboardHighlight={true}
|
||||||
|
physicalKeyboardHighlightPreventDefault={true}
|
||||||
|
physicalKeyboardHighlightTextColor="black"
|
||||||
|
physicalKeyboardHighlightBgColor="lightblue"
|
||||||
/>
|
/>
|
||||||
<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={onKeyDown}
|
||||||
|
onKeyReleased={onKeyUp}
|
||||||
display={keyDisplayMap}
|
display={keyDisplayMap}
|
||||||
layout={{
|
layout={virtualKeyboard.arrows}
|
||||||
default: ["ArrowUp", "ArrowLeft ArrowDown ArrowRight"],
|
disableButtonHold={true}
|
||||||
}}
|
enableLayoutCandidates={false}
|
||||||
syncInstanceInputs={true}
|
preventMouseDownDefault={true}
|
||||||
debug={false}
|
preventMouseUpDefault={true}
|
||||||
|
stopMouseDownPropagation={true}
|
||||||
|
stopMouseUpPropagation={true}
|
||||||
|
physicalKeyboardHighlight={true}
|
||||||
|
physicalKeyboardHighlightPreventDefault={true}
|
||||||
|
physicalKeyboardHighlightTextColor="black"
|
||||||
|
physicalKeyboardHighlightBgColor="lightblue"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{ /* TODO add optional number pad */ }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
|
@ -9,9 +9,8 @@ import notifications from "@/notifications";
|
||||||
import useKeyboard from "@/hooks/useKeyboard";
|
import useKeyboard from "@/hooks/useKeyboard";
|
||||||
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||||
import { cx } from "@/cva.config";
|
import { cx } from "@/cva.config";
|
||||||
import { keys, modifiers } from "@/keyboardMappings";
|
import { keys } from "@/keyboardMappings";
|
||||||
import {
|
import {
|
||||||
useHidStore,
|
|
||||||
useMouseStore,
|
useMouseStore,
|
||||||
useRTCStore,
|
useRTCStore,
|
||||||
useSettingsStore,
|
useSettingsStore,
|
||||||
|
@ -28,15 +27,14 @@ import {
|
||||||
export default function WebRTCVideo() {
|
export default function WebRTCVideo() {
|
||||||
// Video and stream related refs and states
|
// Video and stream related refs and states
|
||||||
const videoElm = useRef<HTMLVideoElement>(null);
|
const videoElm = useRef<HTMLVideoElement>(null);
|
||||||
const mediaStream = useRTCStore(state => state.mediaStream);
|
const { mediaStream, peerConnectionState } = useRTCStore();
|
||||||
const [isPlaying, setIsPlaying] = useState(false);
|
const [isPlaying, setIsPlaying] = useState(false);
|
||||||
const peerConnectionState = useRTCStore(state => state.peerConnectionState);
|
|
||||||
const [isPointerLockActive, setIsPointerLockActive] = useState(false);
|
const [isPointerLockActive, setIsPointerLockActive] = useState(false);
|
||||||
|
const [isKeyboardLockActive, setIsKeyboardLockActive] = useState(false);
|
||||||
// Store hooks
|
// Store hooks
|
||||||
const settings = useSettingsStore();
|
const settings = useSettingsStore();
|
||||||
const { sendKeyboardEvent, resetKeyboardState } = useKeyboard();
|
const { handleKeyPress, resetKeyboardState } = useKeyboard();
|
||||||
const setMousePosition = useMouseStore(state => state.setMousePosition);
|
const { setMousePosition, setMouseMove } = useMouseStore();
|
||||||
const setMouseMove = useMouseStore(state => state.setMouseMove);
|
|
||||||
const {
|
const {
|
||||||
setClientSize: setVideoClientSize,
|
setClientSize: setVideoClientSize,
|
||||||
setSize: setVideoSize,
|
setSize: setVideoSize,
|
||||||
|
@ -44,49 +42,39 @@ export default function WebRTCVideo() {
|
||||||
height: videoHeight,
|
height: videoHeight,
|
||||||
clientWidth: videoClientWidth,
|
clientWidth: videoClientWidth,
|
||||||
clientHeight: videoClientHeight,
|
clientHeight: videoClientHeight,
|
||||||
|
hdmiState,
|
||||||
} = useVideoStore();
|
} = useVideoStore();
|
||||||
|
|
||||||
// Video enhancement settings
|
// Video enhancement settings
|
||||||
const videoSaturation = useSettingsStore(state => state.videoSaturation);
|
const { videoSaturation, videoBrightness, videoContrast } = useSettingsStore();
|
||||||
const videoBrightness = useSettingsStore(state => state.videoBrightness);
|
|
||||||
const videoContrast = useSettingsStore(state => state.videoContrast);
|
|
||||||
|
|
||||||
// HID related states
|
|
||||||
const keyboardLedStateSyncAvailable = useHidStore(state => state.keyboardLedStateSyncAvailable);
|
|
||||||
const keyboardLedSync = useSettingsStore(state => state.keyboardLedSync);
|
|
||||||
const isKeyboardLedManagedByHost = useMemo(() =>
|
|
||||||
keyboardLedSync !== "browser" && keyboardLedStateSyncAvailable,
|
|
||||||
[keyboardLedSync, keyboardLedStateSyncAvailable],
|
|
||||||
);
|
|
||||||
|
|
||||||
const setIsNumLockActive = useHidStore(state => state.setIsNumLockActive);
|
|
||||||
const setIsCapsLockActive = useHidStore(state => state.setIsCapsLockActive);
|
|
||||||
const setIsScrollLockActive = useHidStore(state => state.setIsScrollLockActive);
|
|
||||||
|
|
||||||
// RTC related states
|
// RTC related states
|
||||||
const peerConnection = useRTCStore(state => state.peerConnection);
|
const { peerConnection } = useRTCStore();
|
||||||
|
|
||||||
// HDMI and UI states
|
// HDMI and UI states
|
||||||
const hdmiState = useVideoStore(state => state.hdmiState);
|
|
||||||
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;
|
||||||
|
|
||||||
|
// Mouse wheel states
|
||||||
const [blockWheelEvent, setBlockWheelEvent] = useState(false);
|
const [blockWheelEvent, setBlockWheelEvent] = useState(false);
|
||||||
|
|
||||||
// Misc states and hooks
|
// Misc states and hooks
|
||||||
const [send] = useJsonRpc();
|
const { send } = useJsonRpc();
|
||||||
|
|
||||||
// Video-related
|
// Video-related
|
||||||
|
const handleResize = useCallback(
|
||||||
|
( { width, height }: { width: number | undefined; height: number | undefined }) => {
|
||||||
|
if (!videoElm.current) return;
|
||||||
|
// Do something with width and height, e.g.:
|
||||||
|
setVideoClientSize(width || 0, height || 0);
|
||||||
|
setVideoSize(videoElm.current.videoWidth, videoElm.current.videoHeight);
|
||||||
|
},
|
||||||
|
[setVideoClientSize, setVideoSize]
|
||||||
|
);
|
||||||
|
|
||||||
useResizeObserver({
|
useResizeObserver({
|
||||||
ref: videoElm as React.RefObject<HTMLElement>,
|
ref: videoElm as React.RefObject<HTMLElement>,
|
||||||
onResize: ({ width, height }) => {
|
onResize: handleResize,
|
||||||
// This is actually client size, not videoSize
|
|
||||||
if (width && height) {
|
|
||||||
if (!videoElm.current) return;
|
|
||||||
setVideoClientSize(width, height);
|
|
||||||
setVideoSize(videoElm.current.videoWidth, videoElm.current.videoHeight);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const updateVideoSizeStore = useCallback(
|
const updateVideoSizeStore = useCallback(
|
||||||
|
@ -107,7 +95,7 @@ export default function WebRTCVideo() {
|
||||||
function updateVideoSizeOnMount() {
|
function updateVideoSizeOnMount() {
|
||||||
if (videoElm.current) updateVideoSizeStore(videoElm.current);
|
if (videoElm.current) updateVideoSizeStore(videoElm.current);
|
||||||
},
|
},
|
||||||
[setVideoClientSize, updateVideoSizeStore, setVideoSize],
|
[updateVideoSizeStore],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Pointer lock and keyboard lock related
|
// Pointer lock and keyboard lock related
|
||||||
|
@ -115,7 +103,7 @@ export default function WebRTCVideo() {
|
||||||
const isFullscreenEnabled = document.fullscreenEnabled;
|
const isFullscreenEnabled = document.fullscreenEnabled;
|
||||||
|
|
||||||
const checkNavigatorPermissions = useCallback(async (permissionName: string) => {
|
const checkNavigatorPermissions = useCallback(async (permissionName: string) => {
|
||||||
if (!navigator.permissions || !navigator.permissions.query) {
|
if (!navigator || !navigator.permissions || !navigator.permissions.query) {
|
||||||
return false; // if can't query permissions, assume NOT granted
|
return false; // if can't query permissions, assume NOT granted
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -150,28 +138,30 @@ export default function WebRTCVideo() {
|
||||||
|
|
||||||
const isKeyboardLockGranted = await checkNavigatorPermissions("keyboard-lock");
|
const isKeyboardLockGranted = await checkNavigatorPermissions("keyboard-lock");
|
||||||
|
|
||||||
if (isKeyboardLockGranted && "keyboard" in navigator) {
|
if (isKeyboardLockGranted && navigator && "keyboard" in navigator) {
|
||||||
try {
|
try {
|
||||||
// @ts-expect-error - keyboard lock is not supported in all browsers
|
// @ts-expect-error - keyboard lock is not supported in all browsers
|
||||||
await navigator.keyboard.lock();
|
await navigator.keyboard.lock();
|
||||||
|
setIsKeyboardLockActive(true);
|
||||||
} catch {
|
} catch {
|
||||||
// ignore errors
|
// ignore errors
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [checkNavigatorPermissions]);
|
}, [checkNavigatorPermissions, setIsKeyboardLockActive]);
|
||||||
|
|
||||||
const releaseKeyboardLock = useCallback(async () => {
|
const releaseKeyboardLock = useCallback(async () => {
|
||||||
if (videoElm.current === null || document.fullscreenElement !== videoElm.current) return;
|
if (videoElm.current === null || document.fullscreenElement !== videoElm.current) return;
|
||||||
|
|
||||||
if ("keyboard" in navigator) {
|
if (navigator && "keyboard" in navigator) {
|
||||||
try {
|
try {
|
||||||
// @ts-expect-error - keyboard unlock is not supported in all browsers
|
// @ts-expect-error - keyboard unlock is not supported in all browsers
|
||||||
await navigator.keyboard.unlock();
|
await navigator.keyboard.unlock();
|
||||||
} catch {
|
} catch {
|
||||||
// ignore errors
|
// ignore errors
|
||||||
}
|
}
|
||||||
|
setIsKeyboardLockActive(false);
|
||||||
}
|
}
|
||||||
}, []);
|
}, [setIsKeyboardLockActive]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isPointerLockPossible || !videoElm.current) return;
|
if (!isPointerLockPossible || !videoElm.current) return;
|
||||||
|
@ -344,153 +334,58 @@ export default function WebRTCVideo() {
|
||||||
sendAbsMouseMovement(0, 0, 0);
|
sendAbsMouseMovement(0, 0, 0);
|
||||||
}, [sendAbsMouseMovement]);
|
}, [sendAbsMouseMovement]);
|
||||||
|
|
||||||
// Keyboard-related
|
|
||||||
const handleModifierKeys = useCallback(
|
|
||||||
(e: KeyboardEvent, activeModifiers: number[]) => {
|
|
||||||
const { shiftKey, ctrlKey, altKey, metaKey } = e;
|
|
||||||
|
|
||||||
const filteredModifiers = activeModifiers.filter(Boolean);
|
|
||||||
|
|
||||||
// Example: activeModifiers = [0x01, 0x02, 0x04, 0x08]
|
|
||||||
// Assuming 0x01 = ControlLeft, 0x02 = ShiftLeft, 0x04 = AltLeft, 0x08 = MetaLeft
|
|
||||||
return (
|
|
||||||
filteredModifiers
|
|
||||||
// Shift: Keep if Shift is pressed or if the key isn't a Shift key
|
|
||||||
// Example: If shiftKey is true, keep all modifiers
|
|
||||||
// If shiftKey is false, filter out 0x02 (ShiftLeft) and 0x20 (ShiftRight)
|
|
||||||
.filter(
|
|
||||||
modifier =>
|
|
||||||
shiftKey ||
|
|
||||||
(modifier !== modifiers["ShiftLeft"] &&
|
|
||||||
modifier !== modifiers["ShiftRight"]),
|
|
||||||
)
|
|
||||||
// Ctrl: Keep if Ctrl is pressed or if the key isn't a Ctrl key
|
|
||||||
// Example: If ctrlKey is true, keep all modifiers
|
|
||||||
// If ctrlKey is false, filter out 0x01 (ControlLeft) and 0x10 (ControlRight)
|
|
||||||
.filter(
|
|
||||||
modifier =>
|
|
||||||
ctrlKey ||
|
|
||||||
(modifier !== modifiers["ControlLeft"] &&
|
|
||||||
modifier !== modifiers["ControlRight"]),
|
|
||||||
)
|
|
||||||
// Alt: Keep if Alt is pressed or if the key isn't an Alt key
|
|
||||||
// Example: If altKey is true, keep all modifiers
|
|
||||||
// If altKey is false, filter out 0x04 (AltLeft)
|
|
||||||
//
|
|
||||||
// But intentionally do not filter out 0x40 (AltRight) to accomodate
|
|
||||||
// Alt Gr (Alt Graph) as a modifier. Oddly, Alt Gr does not declare
|
|
||||||
// itself to be an altKey. For example, the KeyboardEvent for
|
|
||||||
// Alt Gr + 2 has the following structure:
|
|
||||||
// - altKey: false
|
|
||||||
// - code: "Digit2"
|
|
||||||
// - type: [ "keydown" | "keyup" ]
|
|
||||||
//
|
|
||||||
// For context, filteredModifiers aims to keep track which modifiers
|
|
||||||
// are being pressed on the physical keyboard at any point in time.
|
|
||||||
// There is logic in the keyUpHandler and keyDownHandler to add and
|
|
||||||
// remove 0x40 (AltRight) from the list of new modifiers.
|
|
||||||
//
|
|
||||||
// But relying on the two handlers alone to track the state of the
|
|
||||||
// modifier bears the risk that the key up event for Alt Gr could
|
|
||||||
// get lost while the browser window is temporarily out of focus,
|
|
||||||
// which means the Alt Gr key state would then be "stuck". At this
|
|
||||||
// point, we would need to rely on the user to press Alt Gr again
|
|
||||||
// to properly release the state of that modifier.
|
|
||||||
.filter(modifier => altKey || modifier !== modifiers["AltLeft"])
|
|
||||||
// Meta: Keep if Meta is pressed or if the key isn't a Meta key
|
|
||||||
// Example: If metaKey is true, keep all modifiers
|
|
||||||
// If metaKey is false, filter out 0x08 (MetaLeft) and 0x80 (MetaRight)
|
|
||||||
.filter(
|
|
||||||
modifier =>
|
|
||||||
metaKey ||
|
|
||||||
(modifier !== modifiers["MetaLeft"] && modifier !== modifiers["MetaRight"]),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const keyDownHandler = useCallback(
|
const keyDownHandler = useCallback(
|
||||||
async (e: KeyboardEvent) => {
|
async (e: KeyboardEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const prev = useHidStore.getState();
|
const code = getAdjustedKeyCode(e);
|
||||||
let code = e.code;
|
const hidKey = keys[code];
|
||||||
const key = e.key;
|
|
||||||
|
|
||||||
if (!isKeyboardLedManagedByHost) {
|
if (hidKey === undefined) {
|
||||||
setIsNumLockActive(e.getModifierState("NumLock"));
|
console.warn(`Key down not mapped: ${code}`);
|
||||||
setIsCapsLockActive(e.getModifierState("CapsLock"));
|
return;
|
||||||
setIsScrollLockActive(e.getModifierState("ScrollLock"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (code == "IntlBackslash" && ["`", "~"].includes(key)) {
|
|
||||||
code = "Backquote";
|
|
||||||
} else if (code == "Backquote" && ["§", "±"].includes(key)) {
|
|
||||||
code = "IntlBackslash";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add the key to the active keys
|
|
||||||
const newKeys = [...prev.activeKeys, keys[code]].filter(Boolean);
|
|
||||||
|
|
||||||
// Add the modifier to the active modifiers
|
|
||||||
const newModifiers = handleModifierKeys(e, [
|
|
||||||
...prev.activeModifiers,
|
|
||||||
modifiers[code],
|
|
||||||
]);
|
|
||||||
|
|
||||||
// When pressing the meta key + another key, the key will never trigger a keyup
|
// When pressing the meta key + another key, the key will never trigger a keyup
|
||||||
// event, so we need to clear the keys after a short delay
|
// event, so we need to clear the keys after a short delay
|
||||||
// https://bugs.chromium.org/p/chromium/issues/detail?id=28089
|
// https://bugs.chromium.org/p/chromium/issues/detail?id=28089
|
||||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=1299553
|
// https://bugzilla.mozilla.org/show_bug.cgi?id=1299553
|
||||||
if (e.metaKey) {
|
if (e.metaKey && hidKey < 0xE0) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const prev = useHidStore.getState();
|
console.debug(`Forcing the meta key release of associated key: ${hidKey}`);
|
||||||
sendKeyboardEvent([], newModifiers || prev.activeModifiers);
|
handleKeyPress(hidKey, false);
|
||||||
}, 10);
|
}, 10);
|
||||||
}
|
}
|
||||||
|
console.debug(`Key down: ${hidKey}`);
|
||||||
|
handleKeyPress(hidKey, true);
|
||||||
|
|
||||||
sendKeyboardEvent([...new Set(newKeys)], [...new Set(newModifiers)]);
|
if (!isKeyboardLockActive && hidKey === keys.MetaLeft) {
|
||||||
|
// If the left meta key was just pressed and we're not keyboard locked
|
||||||
|
// we'll never see the keyup event because the browser is going to lose
|
||||||
|
// focus so set a deferred keyup after a short delay
|
||||||
|
setTimeout(() => {
|
||||||
|
console.debug(`Forcing the left meta key release`);
|
||||||
|
handleKeyPress(hidKey, false);
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[
|
[handleKeyPress, isKeyboardLockActive],
|
||||||
handleModifierKeys,
|
|
||||||
sendKeyboardEvent,
|
|
||||||
isKeyboardLedManagedByHost,
|
|
||||||
setIsNumLockActive,
|
|
||||||
setIsCapsLockActive,
|
|
||||||
setIsScrollLockActive,
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const keyUpHandler = useCallback(
|
const keyUpHandler = useCallback(
|
||||||
(e: KeyboardEvent) => {
|
async (e: KeyboardEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const prev = useHidStore.getState();
|
const code = getAdjustedKeyCode(e);
|
||||||
|
const hidKey = keys[code];
|
||||||
|
|
||||||
if (!isKeyboardLedManagedByHost) {
|
if (hidKey === undefined) {
|
||||||
setIsNumLockActive(e.getModifierState("NumLock"));
|
console.warn(`Key up not mapped: ${code}`);
|
||||||
setIsCapsLockActive(e.getModifierState("CapsLock"));
|
return;
|
||||||
setIsScrollLockActive(e.getModifierState("ScrollLock"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filtering out the key that was just released (keys[e.code])
|
console.debug(`Key up: ${hidKey}`);
|
||||||
const newKeys = prev.activeKeys.filter(k => k !== keys[e.code]).filter(Boolean);
|
handleKeyPress(hidKey, false);
|
||||||
|
|
||||||
// Filter out the modifier that was just released
|
|
||||||
const newModifiers = handleModifierKeys(
|
|
||||||
e,
|
|
||||||
prev.activeModifiers.filter(k => k !== modifiers[e.code]),
|
|
||||||
);
|
|
||||||
|
|
||||||
sendKeyboardEvent([...new Set(newKeys)], [...new Set(newModifiers)]);
|
|
||||||
},
|
},
|
||||||
[
|
[handleKeyPress],
|
||||||
handleModifierKeys,
|
|
||||||
sendKeyboardEvent,
|
|
||||||
isKeyboardLedManagedByHost,
|
|
||||||
setIsNumLockActive,
|
|
||||||
setIsCapsLockActive,
|
|
||||||
setIsScrollLockActive,
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const videoKeyUpHandler = useCallback((e: KeyboardEvent) => {
|
const videoKeyUpHandler = useCallback((e: KeyboardEvent) => {
|
||||||
|
@ -501,7 +396,7 @@ export default function WebRTCVideo() {
|
||||||
// Fix only works in chrome based browsers.
|
// Fix only works in chrome based browsers.
|
||||||
if (e.code === "Space") {
|
if (e.code === "Space") {
|
||||||
if (videoElm.current.paused) {
|
if (videoElm.current.paused) {
|
||||||
console.log("Force playing video");
|
console.debug("Force playing video");
|
||||||
videoElm.current.play();
|
videoElm.current.play();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -544,13 +439,7 @@ export default function WebRTCVideo() {
|
||||||
// We set the as early as possible
|
// We set the as early as possible
|
||||||
addStreamToVideoElm(mediaStream);
|
addStreamToVideoElm(mediaStream);
|
||||||
},
|
},
|
||||||
[
|
[addStreamToVideoElm, mediaStream],
|
||||||
setVideoClientSize,
|
|
||||||
mediaStream,
|
|
||||||
updateVideoSizeStore,
|
|
||||||
peerConnection,
|
|
||||||
addStreamToVideoElm,
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Setup Keyboard Events
|
// Setup Keyboard Events
|
||||||
|
@ -606,7 +495,7 @@ export default function WebRTCVideo() {
|
||||||
|
|
||||||
videoElmRefValue.addEventListener("mousemove", isRelativeMouseMode ? relMouseMoveHandler : absMouseMoveHandler, { signal });
|
videoElmRefValue.addEventListener("mousemove", isRelativeMouseMode ? relMouseMoveHandler : absMouseMoveHandler, { signal });
|
||||||
videoElmRefValue.addEventListener("pointerdown", isRelativeMouseMode ? relMouseMoveHandler : absMouseMoveHandler, { signal });
|
videoElmRefValue.addEventListener("pointerdown", isRelativeMouseMode ? relMouseMoveHandler : absMouseMoveHandler, { signal });
|
||||||
videoElmRefValue.addEventListener("pointerup", isRelativeMouseMode ? relMouseMoveHandler :absMouseMoveHandler, { signal });
|
videoElmRefValue.addEventListener("pointerup", isRelativeMouseMode ? relMouseMoveHandler : absMouseMoveHandler, { signal });
|
||||||
videoElmRefValue.addEventListener("wheel", mouseWheelHandler, {
|
videoElmRefValue.addEventListener("wheel", mouseWheelHandler, {
|
||||||
signal,
|
signal,
|
||||||
passive: true,
|
passive: true,
|
||||||
|
@ -667,6 +556,18 @@ export default function WebRTCVideo() {
|
||||||
};
|
};
|
||||||
}, [videoSaturation, videoBrightness, videoContrast]);
|
}, [videoSaturation, videoBrightness, videoContrast]);
|
||||||
|
|
||||||
|
function getAdjustedKeyCode(e: KeyboardEvent) {
|
||||||
|
const key = e.key;
|
||||||
|
let code = e.code;
|
||||||
|
|
||||||
|
if (code == "IntlBackslash" && ["`", "~"].includes(key)) {
|
||||||
|
code = "Backquote";
|
||||||
|
} else if (code == "Backquote" && ["§", "±"].includes(key)) {
|
||||||
|
code = "IntlBackslash";
|
||||||
|
}
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
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">
|
||||||
|
|
|
@ -23,7 +23,7 @@ export function ATXPowerControl() {
|
||||||
> | null>(null);
|
> | null>(null);
|
||||||
const [atxState, setAtxState] = useState<ATXState | null>(null);
|
const [atxState, setAtxState] = useState<ATXState | null>(null);
|
||||||
|
|
||||||
const [send] = useJsonRpc(function onRequest(resp) {
|
const { send } = useJsonRpc(function onRequest(resp) {
|
||||||
if (resp.method === "atxState") {
|
if (resp.method === "atxState") {
|
||||||
setAtxState(resp.params as ATXState);
|
setAtxState(resp.params as ATXState);
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,7 @@ interface DCPowerState {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DCPowerControl() {
|
export function DCPowerControl() {
|
||||||
const [send] = useJsonRpc();
|
const { send } = useJsonRpc();
|
||||||
const [powerState, setPowerState] = useState<DCPowerState | null>(null);
|
const [powerState, setPowerState] = useState<DCPowerState | null>(null);
|
||||||
|
|
||||||
const getDCPowerState = useCallback(() => {
|
const getDCPowerState = useCallback(() => {
|
||||||
|
|
|
@ -17,7 +17,7 @@ interface SerialSettings {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SerialConsole() {
|
export function SerialConsole() {
|
||||||
const [send] = useJsonRpc();
|
const { send } = useJsonRpc();
|
||||||
const [settings, setSettings] = useState<SerialSettings>({
|
const [settings, setSettings] = useState<SerialSettings>({
|
||||||
baudRate: "9600",
|
baudRate: "9600",
|
||||||
dataBits: "8",
|
dataBits: "8",
|
||||||
|
@ -49,7 +49,7 @@ export function SerialConsole() {
|
||||||
setSettings(newSettings);
|
setSettings(newSettings);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
const setTerminalType = useUiStore(state => state.setTerminalType);
|
const { setTerminalType } = useUiStore();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
|
|
@ -39,7 +39,7 @@ const AVAILABLE_EXTENSIONS: Extension[] = [
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function ExtensionPopover() {
|
export default function ExtensionPopover() {
|
||||||
const [send] = useJsonRpc();
|
const { send } = useJsonRpc();
|
||||||
const [activeExtension, setActiveExtension] = useState<Extension | null>(null);
|
const [activeExtension, setActiveExtension] = useState<Extension | null>(null);
|
||||||
|
|
||||||
// Load active extension on component mount
|
// Load active extension on component mount
|
||||||
|
|
|
@ -21,8 +21,8 @@ import { useDeviceUiNavigation } from "@/hooks/useAppNavigation";
|
||||||
import notifications from "@/notifications";
|
import notifications from "@/notifications";
|
||||||
|
|
||||||
const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
||||||
const diskDataChannelStats = useRTCStore(state => state.diskDataChannelStats);
|
const { diskDataChannelStats } = useRTCStore();
|
||||||
const [send] = useJsonRpc();
|
const { send } = useJsonRpc();
|
||||||
const { remoteVirtualMediaState, setModalView, setRemoteVirtualMediaState } =
|
const { remoteVirtualMediaState, setModalView, setRemoteVirtualMediaState } =
|
||||||
useMountMediaStore();
|
useMountMediaStore();
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { LuCornerDownLeft } from "react-icons/lu";
|
import { LuCornerDownLeft } from "react-icons/lu";
|
||||||
import { ExclamationCircleIcon } from "@heroicons/react/16/solid";
|
import { ExclamationCircleIcon } from "@heroicons/react/16/solid";
|
||||||
import { useClose } from "@headlessui/react";
|
import { useClose } from "@headlessui/react";
|
||||||
|
@ -10,7 +10,8 @@ import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||||
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||||
import { useHidStore, useRTCStore, useUiStore, useSettingsStore } from "@/hooks/stores";
|
import { useHidStore, useRTCStore, useUiStore, useSettingsStore } from "@/hooks/stores";
|
||||||
import { keys, modifiers } from "@/keyboardMappings";
|
import { keys, modifiers } from "@/keyboardMappings";
|
||||||
import { KeyStroke, KeyboardLayout, selectedKeyboard } from "@/keyboardLayouts";
|
import { KeyStroke } from "@/keyboardLayouts";
|
||||||
|
import useKeyboardLayout from "@/hooks/useKeyboardLayout";
|
||||||
import notifications from "@/notifications";
|
import notifications from "@/notifications";
|
||||||
|
|
||||||
const hidKeyboardPayload = (modifier: number, keys: number[]) => {
|
const hidKeyboardPayload = (modifier: number, keys: number[]) => {
|
||||||
|
@ -18,33 +19,24 @@ const hidKeyboardPayload = (modifier: number, keys: number[]) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const modifierCode = (shift?: boolean, altRight?: boolean) => {
|
const modifierCode = (shift?: boolean, altRight?: boolean) => {
|
||||||
return (shift ? modifiers["ShiftLeft"] : 0)
|
return (shift ? modifiers.ShiftLeft : 0)
|
||||||
| (altRight ? modifiers["AltRight"] : 0)
|
| (altRight ? modifiers.AltRight : 0)
|
||||||
}
|
}
|
||||||
const noModifier = 0
|
const noModifier = 0
|
||||||
|
|
||||||
export default function PasteModal() {
|
export default function PasteModal() {
|
||||||
const TextAreaRef = useRef<HTMLTextAreaElement>(null);
|
const TextAreaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
const setPasteMode = useHidStore(state => state.setPasteModeEnabled);
|
const { setPasteModeEnabled } = useHidStore();
|
||||||
const setDisableVideoFocusTrap = useUiStore(state => state.setDisableVideoFocusTrap);
|
const { setDisableVideoFocusTrap } = useUiStore();
|
||||||
|
|
||||||
const [send] = useJsonRpc();
|
const { send } = useJsonRpc();
|
||||||
const rpcDataChannel = useRTCStore(state => state.rpcDataChannel);
|
const { rpcDataChannel } = useRTCStore();
|
||||||
|
|
||||||
const [invalidChars, setInvalidChars] = useState<string[]>([]);
|
const [invalidChars, setInvalidChars] = useState<string[]>([]);
|
||||||
const close = useClose();
|
const close = useClose();
|
||||||
|
|
||||||
const keyboardLayout = useSettingsStore(state => state.keyboardLayout);
|
const { setKeyboardLayout } = useSettingsStore();
|
||||||
const setKeyboardLayout = useSettingsStore(
|
const { selectedKeyboard } = useKeyboardLayout();
|
||||||
state => state.setKeyboardLayout,
|
|
||||||
);
|
|
||||||
|
|
||||||
// this ensures we always get the original en_US if it hasn't been set yet
|
|
||||||
const safeKeyboardLayout = useMemo(() => {
|
|
||||||
if (keyboardLayout && keyboardLayout.length > 0)
|
|
||||||
return keyboardLayout;
|
|
||||||
return "en_US";
|
|
||||||
}, [keyboardLayout]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
send("getKeyboardLayout", {}, resp => {
|
send("getKeyboardLayout", {}, resp => {
|
||||||
|
@ -54,24 +46,23 @@ export default function PasteModal() {
|
||||||
}, [send, setKeyboardLayout]);
|
}, [send, setKeyboardLayout]);
|
||||||
|
|
||||||
const onCancelPasteMode = useCallback(() => {
|
const onCancelPasteMode = useCallback(() => {
|
||||||
setPasteMode(false);
|
setPasteModeEnabled(false);
|
||||||
setDisableVideoFocusTrap(false);
|
setDisableVideoFocusTrap(false);
|
||||||
setInvalidChars([]);
|
setInvalidChars([]);
|
||||||
}, [setDisableVideoFocusTrap, setPasteMode]);
|
}, [setDisableVideoFocusTrap, setPasteModeEnabled]);
|
||||||
|
|
||||||
const onConfirmPaste = useCallback(async () => {
|
const onConfirmPaste = useCallback(async () => {
|
||||||
setPasteMode(false);
|
setPasteModeEnabled(false);
|
||||||
setDisableVideoFocusTrap(false);
|
setDisableVideoFocusTrap(false);
|
||||||
|
|
||||||
if (rpcDataChannel?.readyState !== "open" || !TextAreaRef.current) return;
|
if (rpcDataChannel?.readyState !== "open" || !TextAreaRef.current) return;
|
||||||
const keyboard: KeyboardLayout = selectedKeyboard(safeKeyboardLayout);
|
if (!selectedKeyboard) return;
|
||||||
if (!keyboard) return;
|
|
||||||
|
|
||||||
const text = TextAreaRef.current.value;
|
const text = TextAreaRef.current.value;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for (const char of text) {
|
for (const char of text) {
|
||||||
const keyprops = keyboard.chars[char];
|
const keyprops = selectedKeyboard.chars[char];
|
||||||
if (!keyprops) continue;
|
if (!keyprops) continue;
|
||||||
|
|
||||||
const { key, shift, altRight, deadKey, accentKey } = keyprops;
|
const { key, shift, altRight, deadKey, accentKey } = keyprops;
|
||||||
|
@ -111,7 +102,7 @@ export default function PasteModal() {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [rpcDataChannel?.readyState, safeKeyboardLayout, send, setDisableVideoFocusTrap, setPasteMode]);
|
}, [selectedKeyboard, rpcDataChannel?.readyState, send, setDisableVideoFocusTrap, setPasteModeEnabled]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (TextAreaRef.current) {
|
if (TextAreaRef.current) {
|
||||||
|
@ -161,7 +152,7 @@ export default function PasteModal() {
|
||||||
// @ts-expect-error TS doesn't recognize Intl.Segmenter in some environments
|
// @ts-expect-error TS doesn't recognize Intl.Segmenter in some environments
|
||||||
[...new Intl.Segmenter().segment(value)]
|
[...new Intl.Segmenter().segment(value)]
|
||||||
.map(x => x.segment)
|
.map(x => x.segment)
|
||||||
.filter(char => !selectedKeyboard(safeKeyboardLayout).chars[char]),
|
.filter(char => !selectedKeyboard.chars[char]),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -182,7 +173,7 @@ export default function PasteModal() {
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<p className="text-xs text-slate-600 dark:text-slate-400">
|
<p className="text-xs text-slate-600 dark:text-slate-400">
|
||||||
Sending text using keyboard layout: {selectedKeyboard(safeKeyboardLayout).name}
|
Sending text using keyboard layout: {selectedKeyboard.isoCode}-{selectedKeyboard.name}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -14,11 +14,9 @@ import AddDeviceForm from "./AddDeviceForm";
|
||||||
export default function WakeOnLanModal() {
|
export default function WakeOnLanModal() {
|
||||||
const [storedDevices, setStoredDevices] = useState<StoredDevice[]>([]);
|
const [storedDevices, setStoredDevices] = useState<StoredDevice[]>([]);
|
||||||
const [showAddForm, setShowAddForm] = useState(false);
|
const [showAddForm, setShowAddForm] = useState(false);
|
||||||
const setDisableVideoFocusTrap = useUiStore(state => state.setDisableVideoFocusTrap);
|
const { setDisableVideoFocusTrap } = useUiStore();
|
||||||
|
const { rpcDataChannel } = useRTCStore();
|
||||||
const rpcDataChannel = useRTCStore(state => state.rpcDataChannel);
|
const { send } = useJsonRpc();
|
||||||
|
|
||||||
const [send] = useJsonRpc();
|
|
||||||
const close = useClose();
|
const close = useClose();
|
||||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||||
const [addDeviceErrorMessage, setAddDeviceErrorMessage] = useState<string | null>(null);
|
const [addDeviceErrorMessage, setAddDeviceErrorMessage] = useState<string | null>(null);
|
||||||
|
|
|
@ -37,10 +37,18 @@ function createChartArray<T, K extends keyof T>(
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ConnectionStatsSidebar() {
|
export default function ConnectionStatsSidebar() {
|
||||||
const inboundRtpStats = useRTCStore(state => state.inboundRtpStats);
|
const { sidebarView, setSidebarView } = useUiStore();
|
||||||
|
const {
|
||||||
const candidatePairStats = useRTCStore(state => state.candidatePairStats);
|
mediaStream,
|
||||||
const setSidebarView = useUiStore(state => state.setSidebarView);
|
peerConnection,
|
||||||
|
inboundRtpStats,
|
||||||
|
appendInboundRtpStats,
|
||||||
|
candidatePairStats,
|
||||||
|
appendCandidatePairStats,
|
||||||
|
appendLocalCandidateStats,
|
||||||
|
appendRemoteCandidateStats,
|
||||||
|
appendDiskDataChannelStats,
|
||||||
|
} = useRTCStore();
|
||||||
|
|
||||||
function isMetricSupported<T, K extends keyof T>(
|
function isMetricSupported<T, K extends keyof T>(
|
||||||
stream: Map<number, T>,
|
stream: Map<number, T>,
|
||||||
|
@ -49,20 +57,6 @@ export default function ConnectionStatsSidebar() {
|
||||||
return Array.from(stream).some(([, stat]) => stat[metric] !== undefined);
|
return Array.from(stream).some(([, stat]) => stat[metric] !== undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
const appendInboundRtpStats = useRTCStore(state => state.appendInboundRtpStats);
|
|
||||||
const appendIceCandidatePair = useRTCStore(state => state.appendCandidatePairStats);
|
|
||||||
const appendDiskDataChannelStats = useRTCStore(
|
|
||||||
state => state.appendDiskDataChannelStats,
|
|
||||||
);
|
|
||||||
const appendLocalCandidateStats = useRTCStore(state => state.appendLocalCandidateStats);
|
|
||||||
const appendRemoteCandidateStats = useRTCStore(
|
|
||||||
state => state.appendRemoteCandidateStats,
|
|
||||||
);
|
|
||||||
|
|
||||||
const peerConnection = useRTCStore(state => state.peerConnection);
|
|
||||||
const mediaStream = useRTCStore(state => state.mediaStream);
|
|
||||||
const sidebarView = useUiStore(state => state.sidebarView);
|
|
||||||
|
|
||||||
useInterval(function collectWebRTCStats() {
|
useInterval(function collectWebRTCStats() {
|
||||||
(async () => {
|
(async () => {
|
||||||
if (!mediaStream) return;
|
if (!mediaStream) return;
|
||||||
|
@ -80,8 +74,7 @@ export default function ConnectionStatsSidebar() {
|
||||||
successfulLocalCandidateId = report.localCandidateId;
|
successfulLocalCandidateId = report.localCandidateId;
|
||||||
successfulRemoteCandidateId = report.remoteCandidateId;
|
successfulRemoteCandidateId = report.remoteCandidateId;
|
||||||
}
|
}
|
||||||
|
appendCandidatePairStats(report);
|
||||||
appendIceCandidatePair(report);
|
|
||||||
} else if (report.type === "local-candidate") {
|
} else if (report.type === "local-candidate") {
|
||||||
// We only want to append the local candidate stats that were used in nominated candidate pair
|
// We only want to append the local candidate stats that were used in nominated candidate pair
|
||||||
if (successfulLocalCandidateId === report.id) {
|
if (successfulLocalCandidateId === report.id) {
|
||||||
|
|
|
@ -47,12 +47,12 @@ export interface User {
|
||||||
picture?: string;
|
picture?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UserState {
|
export interface UserState {
|
||||||
user: User | null;
|
user: User | null;
|
||||||
setUser: (user: User | null) => void;
|
setUser: (user: User | null) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UIState {
|
export interface UIState {
|
||||||
sidebarView: AvailableSidebarViews | null;
|
sidebarView: AvailableSidebarViews | null;
|
||||||
setSidebarView: (view: AvailableSidebarViews | null) => void;
|
setSidebarView: (view: AvailableSidebarViews | null) => void;
|
||||||
|
|
||||||
|
@ -68,21 +68,21 @@ interface UIState {
|
||||||
setAttachedVirtualKeyboardVisibility: (enabled: boolean) => void;
|
setAttachedVirtualKeyboardVisibility: (enabled: boolean) => void;
|
||||||
|
|
||||||
terminalType: AvailableTerminalTypes;
|
terminalType: AvailableTerminalTypes;
|
||||||
setTerminalType: (enabled: UIState["terminalType"]) => void;
|
setTerminalType: (type: UIState["terminalType"]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useUiStore = create<UIState>(set => ({
|
export const useUiStore = create<UIState>(set => ({
|
||||||
terminalType: "none",
|
terminalType: "none",
|
||||||
setTerminalType: type => set({ terminalType: type }),
|
setTerminalType: (type: UIState["terminalType"]) => set({ terminalType: type }),
|
||||||
|
|
||||||
sidebarView: null,
|
sidebarView: null,
|
||||||
setSidebarView: view => set({ sidebarView: view }),
|
setSidebarView: (view: AvailableSidebarViews | null) => set({ sidebarView: view }),
|
||||||
|
|
||||||
disableVideoFocusTrap: false,
|
disableVideoFocusTrap: false,
|
||||||
setDisableVideoFocusTrap: enabled => set({ disableVideoFocusTrap: enabled }),
|
setDisableVideoFocusTrap: (enabled: boolean) => set({ disableVideoFocusTrap: enabled }),
|
||||||
|
|
||||||
isWakeOnLanModalVisible: false,
|
isWakeOnLanModalVisible: false,
|
||||||
setWakeOnLanModalVisibility: enabled => set({ isWakeOnLanModalVisible: enabled }),
|
setWakeOnLanModalVisibility: (enabled: boolean) => set({ isWakeOnLanModalVisible: enabled }),
|
||||||
|
|
||||||
toggleSidebarView: view =>
|
toggleSidebarView: view =>
|
||||||
set(state => {
|
set(state => {
|
||||||
|
@ -94,11 +94,11 @@ export const useUiStore = create<UIState>(set => ({
|
||||||
}),
|
}),
|
||||||
|
|
||||||
isAttachedVirtualKeyboardVisible: true,
|
isAttachedVirtualKeyboardVisible: true,
|
||||||
setAttachedVirtualKeyboardVisibility: enabled =>
|
setAttachedVirtualKeyboardVisibility: (enabled: boolean) =>
|
||||||
set({ isAttachedVirtualKeyboardVisible: enabled }),
|
set({ isAttachedVirtualKeyboardVisible: enabled }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
interface RTCState {
|
export interface RTCState {
|
||||||
peerConnection: RTCPeerConnection | null;
|
peerConnection: RTCPeerConnection | null;
|
||||||
setPeerConnection: (pc: RTCState["peerConnection"]) => void;
|
setPeerConnection: (pc: RTCState["peerConnection"]) => void;
|
||||||
|
|
||||||
|
@ -118,18 +118,18 @@ interface RTCState {
|
||||||
setMediaStream: (stream: MediaStream) => void;
|
setMediaStream: (stream: MediaStream) => void;
|
||||||
|
|
||||||
videoStreamStats: RTCInboundRtpStreamStats | null;
|
videoStreamStats: RTCInboundRtpStreamStats | null;
|
||||||
appendVideoStreamStats: (state: RTCInboundRtpStreamStats) => void;
|
appendVideoStreamStats: (stats: RTCInboundRtpStreamStats) => void;
|
||||||
videoStreamStatsHistory: Map<number, RTCInboundRtpStreamStats>;
|
videoStreamStatsHistory: Map<number, RTCInboundRtpStreamStats>;
|
||||||
|
|
||||||
isTurnServerInUse: boolean;
|
isTurnServerInUse: boolean;
|
||||||
setTurnServerInUse: (inUse: boolean) => void;
|
setTurnServerInUse: (inUse: boolean) => void;
|
||||||
|
|
||||||
inboundRtpStats: Map<number, RTCInboundRtpStreamStats>;
|
inboundRtpStats: Map<number, RTCInboundRtpStreamStats>;
|
||||||
appendInboundRtpStats: (state: RTCInboundRtpStreamStats) => void;
|
appendInboundRtpStats: (stats: RTCInboundRtpStreamStats) => void;
|
||||||
clearInboundRtpStats: () => void;
|
clearInboundRtpStats: () => void;
|
||||||
|
|
||||||
candidatePairStats: Map<number, RTCIceCandidatePairStats>;
|
candidatePairStats: Map<number, RTCIceCandidatePairStats>;
|
||||||
appendCandidatePairStats: (pair: RTCIceCandidatePairStats) => void;
|
appendCandidatePairStats: (stats: RTCIceCandidatePairStats) => void;
|
||||||
clearCandidatePairStats: () => void;
|
clearCandidatePairStats: () => void;
|
||||||
|
|
||||||
// Remote ICE candidates stat type doesn't exist as of today
|
// Remote ICE candidates stat type doesn't exist as of today
|
||||||
|
@ -141,7 +141,7 @@ interface RTCState {
|
||||||
|
|
||||||
// Disk data channel stats type doesn't exist as of today
|
// Disk data channel stats type doesn't exist as of today
|
||||||
diskDataChannelStats: Map<number, RTCDataChannelStats>;
|
diskDataChannelStats: Map<number, RTCDataChannelStats>;
|
||||||
appendDiskDataChannelStats: (stat: RTCDataChannelStats) => void;
|
appendDiskDataChannelStats: (stats: RTCDataChannelStats) => void;
|
||||||
|
|
||||||
terminalChannel: RTCDataChannel | null;
|
terminalChannel: RTCDataChannel | null;
|
||||||
setTerminalChannel: (channel: RTCDataChannel) => void;
|
setTerminalChannel: (channel: RTCDataChannel) => void;
|
||||||
|
@ -149,78 +149,78 @@ interface RTCState {
|
||||||
|
|
||||||
export const useRTCStore = create<RTCState>(set => ({
|
export const useRTCStore = create<RTCState>(set => ({
|
||||||
peerConnection: null,
|
peerConnection: null,
|
||||||
setPeerConnection: pc => set({ peerConnection: pc }),
|
setPeerConnection: (pc: RTCState["peerConnection"]) => set({ peerConnection: pc }),
|
||||||
|
|
||||||
rpcDataChannel: null,
|
rpcDataChannel: null,
|
||||||
setRpcDataChannel: channel => set({ rpcDataChannel: channel }),
|
setRpcDataChannel: (channel: RTCDataChannel) => set({ rpcDataChannel: channel }),
|
||||||
|
|
||||||
transceiver: null,
|
transceiver: null,
|
||||||
setTransceiver: transceiver => set({ transceiver }),
|
setTransceiver: (transceiver: RTCRtpTransceiver) => set({ transceiver }),
|
||||||
|
|
||||||
peerConnectionState: null,
|
peerConnectionState: null,
|
||||||
setPeerConnectionState: state => set({ peerConnectionState: state }),
|
setPeerConnectionState: (state: RTCPeerConnectionState) => set({ peerConnectionState: state }),
|
||||||
|
|
||||||
diskChannel: null,
|
diskChannel: null,
|
||||||
setDiskChannel: channel => set({ diskChannel: channel }),
|
setDiskChannel: (channel: RTCDataChannel) => set({ diskChannel: channel }),
|
||||||
|
|
||||||
mediaStream: null,
|
mediaStream: null,
|
||||||
setMediaStream: stream => set({ mediaStream: stream }),
|
setMediaStream: (stream: MediaStream) => set({ mediaStream: stream }),
|
||||||
|
|
||||||
videoStreamStats: null,
|
videoStreamStats: null,
|
||||||
appendVideoStreamStats: stats => set({ videoStreamStats: stats }),
|
appendVideoStreamStats: (stats: RTCInboundRtpStreamStats) => set({ videoStreamStats: stats }),
|
||||||
videoStreamStatsHistory: new Map(),
|
videoStreamStatsHistory: new Map(),
|
||||||
|
|
||||||
isTurnServerInUse: false,
|
isTurnServerInUse: false,
|
||||||
setTurnServerInUse: inUse => set({ isTurnServerInUse: inUse }),
|
setTurnServerInUse: (inUse: boolean) => set({ isTurnServerInUse: inUse }),
|
||||||
|
|
||||||
inboundRtpStats: new Map(),
|
inboundRtpStats: new Map(),
|
||||||
appendInboundRtpStats: newStat => {
|
appendInboundRtpStats: (stats: RTCInboundRtpStreamStats) => {
|
||||||
set(prevState => ({
|
set(prevState => ({
|
||||||
inboundRtpStats: appendStatToMap(newStat, prevState.inboundRtpStats),
|
inboundRtpStats: appendStatToMap(stats, prevState.inboundRtpStats),
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
clearInboundRtpStats: () => set({ inboundRtpStats: new Map() }),
|
clearInboundRtpStats: () => set({ inboundRtpStats: new Map() }),
|
||||||
|
|
||||||
candidatePairStats: new Map(),
|
candidatePairStats: new Map(),
|
||||||
appendCandidatePairStats: newStat => {
|
appendCandidatePairStats: (stats: RTCIceCandidatePairStats) => {
|
||||||
set(prevState => ({
|
set(prevState => ({
|
||||||
candidatePairStats: appendStatToMap(newStat, prevState.candidatePairStats),
|
candidatePairStats: appendStatToMap(stats, prevState.candidatePairStats),
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
clearCandidatePairStats: () => set({ candidatePairStats: new Map() }),
|
clearCandidatePairStats: () => set({ candidatePairStats: new Map() }),
|
||||||
|
|
||||||
localCandidateStats: new Map(),
|
localCandidateStats: new Map(),
|
||||||
appendLocalCandidateStats: newStat => {
|
appendLocalCandidateStats: (stats: RTCIceCandidateStats) => {
|
||||||
set(prevState => ({
|
set(prevState => ({
|
||||||
localCandidateStats: appendStatToMap(newStat, prevState.localCandidateStats),
|
localCandidateStats: appendStatToMap(stats, prevState.localCandidateStats),
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
|
||||||
remoteCandidateStats: new Map(),
|
remoteCandidateStats: new Map(),
|
||||||
appendRemoteCandidateStats: newStat => {
|
appendRemoteCandidateStats: (stats: RTCIceCandidateStats) => {
|
||||||
set(prevState => ({
|
set(prevState => ({
|
||||||
remoteCandidateStats: appendStatToMap(newStat, prevState.remoteCandidateStats),
|
remoteCandidateStats: appendStatToMap(stats, prevState.remoteCandidateStats),
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
|
||||||
diskDataChannelStats: new Map(),
|
diskDataChannelStats: new Map(),
|
||||||
appendDiskDataChannelStats: newStat => {
|
appendDiskDataChannelStats: (stats: RTCDataChannelStats) => {
|
||||||
set(prevState => ({
|
set(prevState => ({
|
||||||
diskDataChannelStats: appendStatToMap(newStat, prevState.diskDataChannelStats),
|
diskDataChannelStats: appendStatToMap(stats, prevState.diskDataChannelStats),
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
|
||||||
// Add these new properties to the store implementation
|
// Add these new properties to the store implementation
|
||||||
terminalChannel: null,
|
terminalChannel: null,
|
||||||
setTerminalChannel: channel => set({ terminalChannel: channel }),
|
setTerminalChannel: (channel: RTCDataChannel) => set({ terminalChannel: channel }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
interface MouseMove {
|
export interface MouseMove {
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
buttons: number;
|
buttons: number;
|
||||||
}
|
}
|
||||||
interface MouseState {
|
export interface MouseState {
|
||||||
mouseX: number;
|
mouseX: number;
|
||||||
mouseY: number;
|
mouseY: number;
|
||||||
mouseMove?: MouseMove;
|
mouseMove?: MouseMove;
|
||||||
|
@ -232,9 +232,17 @@ export const useMouseStore = create<MouseState>(set => ({
|
||||||
mouseX: 0,
|
mouseX: 0,
|
||||||
mouseY: 0,
|
mouseY: 0,
|
||||||
setMouseMove: (move?: MouseMove) => set({ mouseMove: move }),
|
setMouseMove: (move?: MouseMove) => set({ mouseMove: move }),
|
||||||
setMousePosition: (x, y) => set({ mouseX: x, mouseY: y }),
|
setMousePosition: (x: number, y: number) => set({ mouseX: x, mouseY: y }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
export type HdmiStates = "ready" | "no_signal" | "no_lock" | "out_of_range" | "connecting";
|
||||||
|
export type HdmiErrorStates = Extract<VideoState["hdmiState"], "no_signal" | "no_lock" | "out_of_range">
|
||||||
|
|
||||||
|
export interface HdmiState {
|
||||||
|
ready: boolean;
|
||||||
|
error?: HdmiErrorStates;
|
||||||
|
}
|
||||||
|
|
||||||
export interface VideoState {
|
export interface VideoState {
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
|
@ -242,19 +250,13 @@ export interface VideoState {
|
||||||
clientHeight: number;
|
clientHeight: number;
|
||||||
setClientSize: (width: number, height: number) => void;
|
setClientSize: (width: number, height: number) => void;
|
||||||
setSize: (width: number, height: number) => void;
|
setSize: (width: number, height: number) => void;
|
||||||
hdmiState: "ready" | "no_signal" | "no_lock" | "out_of_range" | "connecting";
|
hdmiState: HdmiStates;
|
||||||
setHdmiState: (state: {
|
setHdmiState: (state: {
|
||||||
ready: boolean;
|
ready: boolean;
|
||||||
error?: Extract<VideoState["hdmiState"], "no_signal" | "no_lock" | "out_of_range">;
|
error?: HdmiErrorStates;
|
||||||
}) => void;
|
}) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BacklightSettings {
|
|
||||||
max_brightness: number;
|
|
||||||
dim_after: number;
|
|
||||||
off_after: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useVideoStore = create<VideoState>(set => ({
|
export const useVideoStore = create<VideoState>(set => ({
|
||||||
width: 0,
|
width: 0,
|
||||||
height: 0,
|
height: 0,
|
||||||
|
@ -263,13 +265,13 @@ export const useVideoStore = create<VideoState>(set => ({
|
||||||
clientHeight: 0,
|
clientHeight: 0,
|
||||||
|
|
||||||
// The video element's client size
|
// The video element's client size
|
||||||
setClientSize: (clientWidth, clientHeight) => set({ clientWidth, clientHeight }),
|
setClientSize: (clientWidth: number, clientHeight: number) => set({ clientWidth, clientHeight }),
|
||||||
|
|
||||||
// Resolution
|
// Resolution
|
||||||
setSize: (width, height) => set({ width, height }),
|
setSize: (width: number, height: number) => set({ width, height }),
|
||||||
|
|
||||||
hdmiState: "connecting",
|
hdmiState: "connecting",
|
||||||
setHdmiState: state => {
|
setHdmiState: (state: HdmiState) => {
|
||||||
if (!state) return;
|
if (!state) return;
|
||||||
const { ready, error } = state;
|
const { ready, error } = state;
|
||||||
|
|
||||||
|
@ -283,9 +285,13 @@ export const useVideoStore = create<VideoState>(set => ({
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export type KeyboardLedSync = "auto" | "browser" | "host";
|
export interface BacklightSettings {
|
||||||
|
max_brightness: number;
|
||||||
|
dim_after: number;
|
||||||
|
off_after: number;
|
||||||
|
}
|
||||||
|
|
||||||
interface SettingsState {
|
export interface SettingsState {
|
||||||
isCursorHidden: boolean;
|
isCursorHidden: boolean;
|
||||||
setCursorVisibility: (enabled: boolean) => void;
|
setCursorVisibility: (enabled: boolean) => void;
|
||||||
|
|
||||||
|
@ -308,9 +314,6 @@ interface SettingsState {
|
||||||
keyboardLayout: string;
|
keyboardLayout: string;
|
||||||
setKeyboardLayout: (layout: string) => void;
|
setKeyboardLayout: (layout: string) => void;
|
||||||
|
|
||||||
keyboardLedSync: KeyboardLedSync;
|
|
||||||
setKeyboardLedSync: (sync: KeyboardLedSync) => void;
|
|
||||||
|
|
||||||
scrollThrottling: number;
|
scrollThrottling: number;
|
||||||
setScrollThrottling: (value: number) => void;
|
setScrollThrottling: (value: number) => void;
|
||||||
|
|
||||||
|
@ -330,17 +333,17 @@ export const useSettingsStore = create(
|
||||||
persist<SettingsState>(
|
persist<SettingsState>(
|
||||||
set => ({
|
set => ({
|
||||||
isCursorHidden: false,
|
isCursorHidden: false,
|
||||||
setCursorVisibility: enabled => set({ isCursorHidden: enabled }),
|
setCursorVisibility: (enabled: boolean) => set({ isCursorHidden: enabled }),
|
||||||
|
|
||||||
mouseMode: "absolute",
|
mouseMode: "absolute",
|
||||||
setMouseMode: mode => set({ mouseMode: mode }),
|
setMouseMode: (mode: string) => set({ mouseMode: mode }),
|
||||||
|
|
||||||
debugMode: import.meta.env.DEV,
|
debugMode: import.meta.env.DEV,
|
||||||
setDebugMode: enabled => set({ debugMode: enabled }),
|
setDebugMode: (enabled: boolean) => set({ debugMode: enabled }),
|
||||||
|
|
||||||
// Add developer mode with default value
|
// Add developer mode with default value
|
||||||
developerMode: false,
|
developerMode: false,
|
||||||
setDeveloperMode: enabled => set({ developerMode: enabled }),
|
setDeveloperMode: (enabled: boolean) => set({ developerMode: enabled }),
|
||||||
|
|
||||||
displayRotation: "270",
|
displayRotation: "270",
|
||||||
setDisplayRotation: (rotation: string) => set({ displayRotation: rotation }),
|
setDisplayRotation: (rotation: string) => set({ displayRotation: rotation }),
|
||||||
|
@ -354,24 +357,21 @@ export const useSettingsStore = create(
|
||||||
set({ backlightSettings: settings }),
|
set({ backlightSettings: settings }),
|
||||||
|
|
||||||
keyboardLayout: "en-US",
|
keyboardLayout: "en-US",
|
||||||
setKeyboardLayout: layout => set({ keyboardLayout: layout }),
|
setKeyboardLayout: (layout: string) => set({ keyboardLayout: layout }),
|
||||||
|
|
||||||
keyboardLedSync: "auto",
|
|
||||||
setKeyboardLedSync: sync => set({ keyboardLedSync: sync }),
|
|
||||||
|
|
||||||
scrollThrottling: 0,
|
scrollThrottling: 0,
|
||||||
setScrollThrottling: value => set({ scrollThrottling: value }),
|
setScrollThrottling: (value: number) => set({ scrollThrottling: value }),
|
||||||
|
|
||||||
showPressedKeys: true,
|
showPressedKeys: true,
|
||||||
setShowPressedKeys: show => set({ showPressedKeys: show }),
|
setShowPressedKeys: (show: boolean) => set({ showPressedKeys: show }),
|
||||||
|
|
||||||
// Video enhancement settings with default values (1.0 = normal)
|
// Video enhancement settings with default values (1.0 = normal)
|
||||||
videoSaturation: 1.0,
|
videoSaturation: 1.0,
|
||||||
setVideoSaturation: value => set({ videoSaturation: value }),
|
setVideoSaturation: (value: number) => set({ videoSaturation: value }),
|
||||||
videoBrightness: 1.0,
|
videoBrightness: 1.0,
|
||||||
setVideoBrightness: value => set({ videoBrightness: value }),
|
setVideoBrightness: (value: number) => set({ videoBrightness: value }),
|
||||||
videoContrast: 1.0,
|
videoContrast: 1.0,
|
||||||
setVideoContrast: value => set({ videoContrast: value }),
|
setVideoContrast: (value: number) => set({ videoContrast: value }),
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: "settings",
|
name: "settings",
|
||||||
|
@ -411,23 +411,23 @@ export interface MountMediaState {
|
||||||
|
|
||||||
export const useMountMediaStore = create<MountMediaState>(set => ({
|
export const useMountMediaStore = create<MountMediaState>(set => ({
|
||||||
localFile: null,
|
localFile: null,
|
||||||
setLocalFile: file => set({ localFile: file }),
|
setLocalFile: (file: MountMediaState["localFile"]) => set({ localFile: file }),
|
||||||
|
|
||||||
remoteVirtualMediaState: null,
|
remoteVirtualMediaState: null,
|
||||||
setRemoteVirtualMediaState: state => set({ remoteVirtualMediaState: state }),
|
setRemoteVirtualMediaState: (state: MountMediaState["remoteVirtualMediaState"]) => set({ remoteVirtualMediaState: state }),
|
||||||
|
|
||||||
modalView: "mode",
|
modalView: "mode",
|
||||||
setModalView: view => set({ modalView: view }),
|
setModalView: (view: MountMediaState["modalView"]) => set({ modalView: view }),
|
||||||
|
|
||||||
isMountMediaDialogOpen: false,
|
isMountMediaDialogOpen: false,
|
||||||
setIsMountMediaDialogOpen: isOpen => set({ isMountMediaDialogOpen: isOpen }),
|
setIsMountMediaDialogOpen: (isOpen: MountMediaState["isMountMediaDialogOpen"]) => set({ isMountMediaDialogOpen: isOpen }),
|
||||||
|
|
||||||
uploadedFiles: [],
|
uploadedFiles: [],
|
||||||
addUploadedFile: file =>
|
addUploadedFile: (file: { name: string; size: string; uploadedAt: string }) =>
|
||||||
set(state => ({ uploadedFiles: [...state.uploadedFiles, file] })),
|
set(state => ({ uploadedFiles: [...state.uploadedFiles, file] })),
|
||||||
|
|
||||||
errorMessage: null,
|
errorMessage: null,
|
||||||
setErrorMessage: message => set({ errorMessage: message }),
|
setErrorMessage: (message: string | null) => set({ errorMessage: message }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export interface KeyboardLedState {
|
export interface KeyboardLedState {
|
||||||
|
@ -436,41 +436,33 @@ export interface KeyboardLedState {
|
||||||
scroll_lock: boolean;
|
scroll_lock: boolean;
|
||||||
compose: boolean;
|
compose: boolean;
|
||||||
kana: boolean;
|
kana: boolean;
|
||||||
|
shift: boolean; // Optional, as not all keyboards have a shift LED
|
||||||
};
|
};
|
||||||
const defaultKeyboardLedState: KeyboardLedState = {
|
|
||||||
num_lock: false,
|
export const hidKeyBufferSize = 6;
|
||||||
caps_lock: false,
|
export const hidErrorRollOver = 0x01;
|
||||||
scroll_lock: false,
|
|
||||||
compose: false,
|
export interface KeysDownState {
|
||||||
kana: false,
|
modifier: number;
|
||||||
};
|
keys: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type USBStates =
|
||||||
|
| "configured"
|
||||||
|
| "attached"
|
||||||
|
| "not attached"
|
||||||
|
| "suspended"
|
||||||
|
| "addressed";
|
||||||
|
|
||||||
export interface HidState {
|
export interface HidState {
|
||||||
activeKeys: number[];
|
keyboardLedState: KeyboardLedState;
|
||||||
activeModifiers: number[];
|
|
||||||
|
|
||||||
updateActiveKeysAndModifiers: (keysAndModifiers: {
|
|
||||||
keys: number[];
|
|
||||||
modifiers: number[];
|
|
||||||
}) => void;
|
|
||||||
|
|
||||||
altGrArmed: boolean;
|
|
||||||
setAltGrArmed: (armed: boolean) => void;
|
|
||||||
|
|
||||||
altGrTimer: number | null; // _altGrCtrlTime
|
|
||||||
setAltGrTimer: (timeout: number | null) => void;
|
|
||||||
|
|
||||||
altGrCtrlTime: number; // _altGrCtrlTime
|
|
||||||
setAltGrCtrlTime: (time: number) => void;
|
|
||||||
|
|
||||||
keyboardLedState?: KeyboardLedState;
|
|
||||||
setKeyboardLedState: (state: KeyboardLedState) => void;
|
setKeyboardLedState: (state: KeyboardLedState) => void;
|
||||||
setIsNumLockActive: (active: boolean) => void;
|
|
||||||
setIsCapsLockActive: (active: boolean) => void;
|
|
||||||
setIsScrollLockActive: (active: boolean) => void;
|
|
||||||
|
|
||||||
keyboardLedStateSyncAvailable: boolean;
|
keysDownState: KeysDownState;
|
||||||
setKeyboardLedStateSyncAvailable: (available: boolean) => void;
|
setKeysDownState: (state: KeysDownState) => void;
|
||||||
|
|
||||||
|
keyPressReportApiAvailable: boolean;
|
||||||
|
setkeyPressReportApiAvailable: (available: boolean) => void;
|
||||||
|
|
||||||
isVirtualKeyboardEnabled: boolean;
|
isVirtualKeyboardEnabled: boolean;
|
||||||
setVirtualKeyboardEnabled: (enabled: boolean) => void;
|
setVirtualKeyboardEnabled: (enabled: boolean) => void;
|
||||||
|
@ -478,55 +470,29 @@ export interface HidState {
|
||||||
isPasteModeEnabled: boolean;
|
isPasteModeEnabled: boolean;
|
||||||
setPasteModeEnabled: (enabled: boolean) => void;
|
setPasteModeEnabled: (enabled: boolean) => void;
|
||||||
|
|
||||||
usbState: "configured" | "attached" | "not attached" | "suspended" | "addressed";
|
usbState: USBStates;
|
||||||
setUsbState: (state: HidState["usbState"]) => void;
|
setUsbState: (state: USBStates) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useHidStore = create<HidState>((set, get) => ({
|
export const useHidStore = create<HidState>(set => ({
|
||||||
activeKeys: [],
|
keyboardLedState: {} as KeyboardLedState,
|
||||||
activeModifiers: [],
|
setKeyboardLedState: (ledState: KeyboardLedState): void => set({ keyboardLedState: ledState }),
|
||||||
updateActiveKeysAndModifiers: ({ keys, modifiers }) => {
|
|
||||||
return set({ activeKeys: keys, activeModifiers: modifiers });
|
|
||||||
},
|
|
||||||
|
|
||||||
altGrArmed: false,
|
keysDownState: { modifier: 0, keys: [0,0,0,0,0,0] } as KeysDownState,
|
||||||
setAltGrArmed: armed => set({ altGrArmed: armed }),
|
setKeysDownState: (state: KeysDownState): void => set({ keysDownState: state }),
|
||||||
|
|
||||||
altGrTimer: 0,
|
keyPressReportApiAvailable: true,
|
||||||
setAltGrTimer: timeout => set({ altGrTimer: timeout }),
|
setkeyPressReportApiAvailable: (available: boolean) => set({ keyPressReportApiAvailable: available }),
|
||||||
|
|
||||||
altGrCtrlTime: 0,
|
|
||||||
setAltGrCtrlTime: time => set({ altGrCtrlTime: time }),
|
|
||||||
|
|
||||||
setKeyboardLedState: ledState => set({ keyboardLedState: ledState }),
|
|
||||||
setIsNumLockActive: active => {
|
|
||||||
const keyboardLedState = { ...(get().keyboardLedState || defaultKeyboardLedState) };
|
|
||||||
keyboardLedState.num_lock = active;
|
|
||||||
set({ keyboardLedState });
|
|
||||||
},
|
|
||||||
setIsCapsLockActive: active => {
|
|
||||||
const keyboardLedState = { ...(get().keyboardLedState || defaultKeyboardLedState) };
|
|
||||||
keyboardLedState.caps_lock = active;
|
|
||||||
set({ keyboardLedState });
|
|
||||||
},
|
|
||||||
setIsScrollLockActive: active => {
|
|
||||||
const keyboardLedState = { ...(get().keyboardLedState || defaultKeyboardLedState) };
|
|
||||||
keyboardLedState.scroll_lock = active;
|
|
||||||
set({ keyboardLedState });
|
|
||||||
},
|
|
||||||
|
|
||||||
keyboardLedStateSyncAvailable: false,
|
|
||||||
setKeyboardLedStateSyncAvailable: available => set({ keyboardLedStateSyncAvailable: available }),
|
|
||||||
|
|
||||||
isVirtualKeyboardEnabled: false,
|
isVirtualKeyboardEnabled: false,
|
||||||
setVirtualKeyboardEnabled: enabled => set({ isVirtualKeyboardEnabled: enabled }),
|
setVirtualKeyboardEnabled: (enabled: boolean): void => set({ isVirtualKeyboardEnabled: enabled }),
|
||||||
|
|
||||||
isPasteModeEnabled: false,
|
isPasteModeEnabled: false,
|
||||||
setPasteModeEnabled: enabled => set({ isPasteModeEnabled: enabled }),
|
setPasteModeEnabled: (enabled: boolean): void => set({ isPasteModeEnabled: enabled }),
|
||||||
|
|
||||||
// Add these new properties for USB state
|
// Add these new properties for USB state
|
||||||
usbState: "not attached",
|
usbState: "not attached",
|
||||||
setUsbState: state => set({ usbState: state }),
|
setUsbState: (state: USBStates) => set({ usbState: state }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const useUserStore = create<UserState>(set => ({
|
export const useUserStore = create<UserState>(set => ({
|
||||||
|
@ -534,11 +500,15 @@ export const useUserStore = create<UserState>(set => ({
|
||||||
setUser: user => set({ user }),
|
setUser: user => set({ user }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export interface UpdateState {
|
export type UpdateModalViews =
|
||||||
isUpdatePending: boolean;
|
| "loading"
|
||||||
setIsUpdatePending: (isPending: boolean) => void;
|
| "updating"
|
||||||
updateDialogHasBeenMinimized: boolean;
|
| "upToDate"
|
||||||
otaState: {
|
| "updateAvailable"
|
||||||
|
| "updateCompleted"
|
||||||
|
| "error";
|
||||||
|
|
||||||
|
export interface OtaState {
|
||||||
updating: boolean;
|
updating: boolean;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
|
|
||||||
|
@ -567,24 +537,24 @@ export interface UpdateState {
|
||||||
|
|
||||||
systemUpdateProgress: number;
|
systemUpdateProgress: number;
|
||||||
systemUpdatedAt: string | null;
|
systemUpdatedAt: string | null;
|
||||||
};
|
};
|
||||||
setOtaState: (state: UpdateState["otaState"]) => void;
|
|
||||||
|
export interface UpdateState {
|
||||||
|
isUpdatePending: boolean;
|
||||||
|
setIsUpdatePending: (isPending: boolean) => void;
|
||||||
|
updateDialogHasBeenMinimized: boolean;
|
||||||
|
otaState: OtaState;
|
||||||
|
setOtaState: (state: OtaState) => void;
|
||||||
setUpdateDialogHasBeenMinimized: (hasBeenMinimized: boolean) => void;
|
setUpdateDialogHasBeenMinimized: (hasBeenMinimized: boolean) => void;
|
||||||
modalView:
|
modalView: UpdateModalViews
|
||||||
| "loading"
|
setModalView: (view: UpdateModalViews) => void;
|
||||||
| "updating"
|
|
||||||
| "upToDate"
|
|
||||||
| "updateAvailable"
|
|
||||||
| "updateCompleted"
|
|
||||||
| "error";
|
|
||||||
setModalView: (view: UpdateState["modalView"]) => void;
|
|
||||||
setUpdateErrorMessage: (errorMessage: string) => void;
|
setUpdateErrorMessage: (errorMessage: string) => void;
|
||||||
updateErrorMessage: string | null;
|
updateErrorMessage: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useUpdateStore = create<UpdateState>(set => ({
|
export const useUpdateStore = create<UpdateState>(set => ({
|
||||||
isUpdatePending: false,
|
isUpdatePending: false,
|
||||||
setIsUpdatePending: isPending => set({ isUpdatePending: isPending }),
|
setIsUpdatePending: (isPending: boolean) => set({ isUpdatePending: isPending }),
|
||||||
|
|
||||||
setOtaState: state => set({ otaState: state }),
|
setOtaState: state => set({ otaState: state }),
|
||||||
otaState: {
|
otaState: {
|
||||||
|
@ -608,18 +578,22 @@ export const useUpdateStore = create<UpdateState>(set => ({
|
||||||
},
|
},
|
||||||
|
|
||||||
updateDialogHasBeenMinimized: false,
|
updateDialogHasBeenMinimized: false,
|
||||||
setUpdateDialogHasBeenMinimized: hasBeenMinimized =>
|
setUpdateDialogHasBeenMinimized: (hasBeenMinimized: boolean) =>
|
||||||
set({ updateDialogHasBeenMinimized: hasBeenMinimized }),
|
set({ updateDialogHasBeenMinimized: hasBeenMinimized }),
|
||||||
modalView: "loading",
|
modalView: "loading",
|
||||||
setModalView: view => set({ modalView: view }),
|
setModalView: (view: UpdateModalViews) => set({ modalView: view }),
|
||||||
updateErrorMessage: null,
|
updateErrorMessage: null,
|
||||||
setUpdateErrorMessage: errorMessage => set({ updateErrorMessage: errorMessage }),
|
setUpdateErrorMessage: (errorMessage: string) => set({ updateErrorMessage: errorMessage }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
interface UsbConfigModalState {
|
export type UsbConfigModalViews =
|
||||||
modalView: "updateUsbConfig" | "updateUsbConfigSuccess";
|
| "updateUsbConfig"
|
||||||
|
| "updateUsbConfigSuccess";
|
||||||
|
|
||||||
|
export interface UsbConfigModalState {
|
||||||
|
modalView: UsbConfigModalViews ;
|
||||||
errorMessage: string | null;
|
errorMessage: string | null;
|
||||||
setModalView: (view: UsbConfigModalState["modalView"]) => void;
|
setModalView: (view: UsbConfigModalViews) => void;
|
||||||
setErrorMessage: (message: string | null) => void;
|
setErrorMessage: (message: string | null) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -634,24 +608,26 @@ export interface UsbConfigState {
|
||||||
export const useUsbConfigModalStore = create<UsbConfigModalState>(set => ({
|
export const useUsbConfigModalStore = create<UsbConfigModalState>(set => ({
|
||||||
modalView: "updateUsbConfig",
|
modalView: "updateUsbConfig",
|
||||||
errorMessage: null,
|
errorMessage: null,
|
||||||
setModalView: view => set({ modalView: view }),
|
setModalView: (view: UsbConfigModalViews) => set({ modalView: view }),
|
||||||
setErrorMessage: message => set({ errorMessage: message }),
|
setErrorMessage: (message: string | null) => set({ errorMessage: message }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
interface LocalAuthModalState {
|
export type LocalAuthModalViews =
|
||||||
modalView:
|
|
||||||
| "createPassword"
|
| "createPassword"
|
||||||
| "deletePassword"
|
| "deletePassword"
|
||||||
| "updatePassword"
|
| "updatePassword"
|
||||||
| "creationSuccess"
|
| "creationSuccess"
|
||||||
| "deleteSuccess"
|
| "deleteSuccess"
|
||||||
| "updateSuccess";
|
| "updateSuccess";
|
||||||
setModalView: (view: LocalAuthModalState["modalView"]) => void;
|
|
||||||
|
export interface LocalAuthModalState {
|
||||||
|
modalView:LocalAuthModalViews;
|
||||||
|
setModalView: (view:LocalAuthModalViews) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useLocalAuthModalStore = create<LocalAuthModalState>(set => ({
|
export const useLocalAuthModalStore = create<LocalAuthModalState>(set => ({
|
||||||
modalView: "createPassword",
|
modalView: "createPassword",
|
||||||
setModalView: view => set({ modalView: view }),
|
setModalView: (view: LocalAuthModalViews) => set({ modalView: view }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export interface DeviceState {
|
export interface DeviceState {
|
||||||
|
@ -666,8 +642,8 @@ export const useDeviceStore = create<DeviceState>(set => ({
|
||||||
appVersion: null,
|
appVersion: null,
|
||||||
systemVersion: null,
|
systemVersion: null,
|
||||||
|
|
||||||
setAppVersion: version => set({ appVersion: version }),
|
setAppVersion: (version: string) => set({ appVersion: version }),
|
||||||
setSystemVersion: version => set({ systemVersion: version }),
|
setSystemVersion: (version: string) => set({ systemVersion: version }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export interface DhcpLease {
|
export interface DhcpLease {
|
||||||
|
@ -833,7 +809,7 @@ export const useMacrosStore = create<MacrosState>((set, get) => ({
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
sendFn("getKeyboardMacros", {}, response => {
|
sendFn("getKeyboardMacros", {}, (response: JsonRpcResponse) => {
|
||||||
if (response.error) {
|
if (response.error) {
|
||||||
console.error("Error loading macros:", response.error);
|
console.error("Error loading macros:", response.error);
|
||||||
reject(new Error(response.error.message));
|
reject(new Error(response.error.message));
|
||||||
|
@ -913,7 +889,7 @@ export const useMacrosStore = create<MacrosState>((set, get) => ({
|
||||||
sendFn(
|
sendFn(
|
||||||
"setKeyboardMacros",
|
"setKeyboardMacros",
|
||||||
{ params: { macros: macrosWithSortOrder } },
|
{ params: { macros: macrosWithSortOrder } },
|
||||||
response => {
|
(response: JsonRpcResponse) => {
|
||||||
resolve(response);
|
resolve(response);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
@ -33,10 +33,10 @@ const callbackStore = new Map<number | string, (resp: JsonRpcResponse) => void>(
|
||||||
let requestCounter = 0;
|
let requestCounter = 0;
|
||||||
|
|
||||||
export function useJsonRpc(onRequest?: (payload: JsonRpcRequest) => void) {
|
export function useJsonRpc(onRequest?: (payload: JsonRpcRequest) => void) {
|
||||||
const rpcDataChannel = useRTCStore(state => state.rpcDataChannel);
|
const { rpcDataChannel } = useRTCStore();
|
||||||
|
|
||||||
const send = useCallback(
|
const send = useCallback(
|
||||||
(method: string, params: unknown, callback?: (resp: JsonRpcResponse) => void) => {
|
async (method: string, params: unknown, callback?: (resp: JsonRpcResponse) => void) => {
|
||||||
if (rpcDataChannel?.readyState !== "open") return;
|
if (rpcDataChannel?.readyState !== "open") return;
|
||||||
requestCounter++;
|
requestCounter++;
|
||||||
const payload = { jsonrpc: "2.0", method, params, id: requestCounter };
|
const payload = { jsonrpc: "2.0", method, params, id: requestCounter };
|
||||||
|
@ -45,7 +45,7 @@ export function useJsonRpc(onRequest?: (payload: JsonRpcRequest) => void) {
|
||||||
|
|
||||||
rpcDataChannel.send(JSON.stringify(payload));
|
rpcDataChannel.send(JSON.stringify(payload));
|
||||||
},
|
},
|
||||||
[rpcDataChannel],
|
[rpcDataChannel]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -61,7 +61,7 @@ export function useJsonRpc(onRequest?: (payload: JsonRpcRequest) => void) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ("error" in payload) console.error(payload.error);
|
if ("error" in payload) console.error("RPC error", payload);
|
||||||
if (!payload.id) return;
|
if (!payload.id) return;
|
||||||
|
|
||||||
const callback = callbackStore.get(payload.id);
|
const callback = callbackStore.get(payload.id);
|
||||||
|
@ -76,7 +76,8 @@ export function useJsonRpc(onRequest?: (payload: JsonRpcRequest) => void) {
|
||||||
return () => {
|
return () => {
|
||||||
rpcDataChannel.removeEventListener("message", messageHandler);
|
rpcDataChannel.removeEventListener("message", messageHandler);
|
||||||
};
|
};
|
||||||
}, [rpcDataChannel, onRequest]);
|
},
|
||||||
|
[rpcDataChannel, onRequest]);
|
||||||
|
|
||||||
return [send];
|
return { send };
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,42 +1,117 @@
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
|
|
||||||
import { useHidStore, useRTCStore } from "@/hooks/stores";
|
import { KeysDownState, useHidStore, useRTCStore, hidKeyBufferSize, hidErrorRollOver } from "@/hooks/stores";
|
||||||
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
||||||
import { keys, modifiers } from "@/keyboardMappings";
|
import { hidKeyToModifierMask, keys, modifiers } from "@/keyboardMappings";
|
||||||
|
|
||||||
export default function useKeyboard() {
|
export default function useKeyboard() {
|
||||||
const [send] = useJsonRpc();
|
const { send } = useJsonRpc();
|
||||||
|
const { rpcDataChannel } = useRTCStore();
|
||||||
|
const { keysDownState, setKeysDownState } = useHidStore();
|
||||||
|
|
||||||
const rpcDataChannel = useRTCStore(state => state.rpcDataChannel);
|
// INTRODUCTION: The earlier version of the JetKVM device shipped with all keyboard state
|
||||||
const updateActiveKeysAndModifiers = useHidStore(
|
// being tracked on the browser/client-side. When adding the keyPressReport API to the
|
||||||
state => state.updateActiveKeysAndModifiers,
|
// device-side code, we have to still support the situation where the browser/client-side code
|
||||||
);
|
// is running on the cloud against a device that has not been updated yet and thus does not
|
||||||
|
// support the keyPressReport API. In that case, we need to handle the key presses locally
|
||||||
|
// and send the full state to the device, so it can behave like a real USB HID keyboard.
|
||||||
|
// This flag indicates whether the keyPressReport API is available on the device which is
|
||||||
|
// dynamically set when the device responds to the first key press event or reports its
|
||||||
|
// keysDownState when queried since the keyPressReport was introduced together with the
|
||||||
|
// getKeysDownState API.
|
||||||
|
const { keyPressReportApiAvailable, setkeyPressReportApiAvailable} = useHidStore();
|
||||||
|
|
||||||
|
// sendKeyboardEvent is used to send the full keyboard state to the device for macro handling
|
||||||
|
// and resetting keyboard state. It sends the keys currently pressed and the modifier state.
|
||||||
|
// The device will respond with the keysDownState if it supports the keyPressReport API
|
||||||
|
// or just accept the state if it does not support (returning no result)
|
||||||
const sendKeyboardEvent = useCallback(
|
const sendKeyboardEvent = useCallback(
|
||||||
(keys: number[], modifiers: number[]) => {
|
async (state: KeysDownState) => {
|
||||||
if (rpcDataChannel?.readyState !== "open") return;
|
if (rpcDataChannel?.readyState !== "open") return;
|
||||||
const accModifier = modifiers.reduce((acc, val) => acc + val, 0);
|
|
||||||
|
|
||||||
send("keyboardReport", { keys, modifier: accModifier });
|
console.debug(`Send keyboardReport keys: ${state.keys}, modifier: ${state.modifier}`);
|
||||||
|
send("keyboardReport", { keys: state.keys, modifier: state.modifier }, (resp: JsonRpcResponse) => {
|
||||||
|
if ("error" in resp) {
|
||||||
|
console.error(`Failed to send keyboard report ${state}`, resp.error);
|
||||||
|
} else {
|
||||||
|
// If the device supports keyPressReport API, it will (also) return the keysDownState when we send
|
||||||
|
// the keyboardReport
|
||||||
|
const keysDownState = resp.result as KeysDownState;
|
||||||
|
|
||||||
// We do this for the info bar to display the currently pressed keys for the user
|
if (keysDownState) {
|
||||||
updateActiveKeysAndModifiers({ keys: keys, modifiers: modifiers });
|
setKeysDownState(keysDownState); // treat the response as the canonical state
|
||||||
|
setkeyPressReportApiAvailable(true); // if they returned a keysDownState, we ALSO know they also support keyPressReport
|
||||||
|
} else {
|
||||||
|
// older devices versions do not return the keyDownState
|
||||||
|
// so we just pretend they accepted what we sent
|
||||||
|
setKeysDownState(state);
|
||||||
|
setkeyPressReportApiAvailable(false); // we ALSO know they do not support keyPressReport
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
[rpcDataChannel?.readyState, send, updateActiveKeysAndModifiers],
|
[rpcDataChannel?.readyState, send, setKeysDownState, setkeyPressReportApiAvailable],
|
||||||
);
|
);
|
||||||
|
|
||||||
const resetKeyboardState = useCallback(() => {
|
// sendKeypressEvent is used to send a single key press/release event to the device.
|
||||||
sendKeyboardEvent([], []);
|
// It sends the key and whether it is pressed or released.
|
||||||
}, [sendKeyboardEvent]);
|
// Older device version will not understand this request and will respond with
|
||||||
|
// an error with code -32601, which means that the RPC method name was not recognized.
|
||||||
|
// In that case we will switch to local key handling and update the keysDownState
|
||||||
|
// in client/browser-side code using simulateDeviceSideKeyHandlingForLegacyDevices.
|
||||||
|
const sendKeypressEvent = useCallback(
|
||||||
|
async (key: number, press: boolean) => {
|
||||||
|
if (rpcDataChannel?.readyState !== "open") return;
|
||||||
|
|
||||||
|
console.debug(`Send keypressEvent key: ${key}, press: ${press}`);
|
||||||
|
send("keypressReport", { key, press }, (resp: JsonRpcResponse) => {
|
||||||
|
if ("error" in resp) {
|
||||||
|
// -32601 means the method is not supported because the device is running an older version
|
||||||
|
if (resp.error.code === -32601) {
|
||||||
|
console.error("Legacy device does not support keypressReport API, switching to local key down state handling", resp.error);
|
||||||
|
setkeyPressReportApiAvailable(false);
|
||||||
|
} else {
|
||||||
|
console.error(`Failed to send key ${key} press: ${press}`, resp.error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const keysDownState = resp.result as KeysDownState;
|
||||||
|
|
||||||
|
if (keysDownState) {
|
||||||
|
setKeysDownState(keysDownState);
|
||||||
|
// we don't need to set keyPressReportApiAvailable here, because it's already true or we never landed here
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[rpcDataChannel?.readyState, send, setkeyPressReportApiAvailable, setKeysDownState],
|
||||||
|
);
|
||||||
|
|
||||||
|
// resetKeyboardState is used to reset the keyboard state to no keys pressed and no modifiers.
|
||||||
|
// This is useful for macros and when the browser loses focus to ensure that the keyboard state
|
||||||
|
// is clean.
|
||||||
|
const resetKeyboardState = useCallback(
|
||||||
|
async () => {
|
||||||
|
// Reset the keys buffer to zeros and the modifier state to zero
|
||||||
|
keysDownState.keys.length = hidKeyBufferSize;
|
||||||
|
keysDownState.keys.fill(0);
|
||||||
|
keysDownState.modifier = 0;
|
||||||
|
sendKeyboardEvent(keysDownState);
|
||||||
|
}, [keysDownState, sendKeyboardEvent]);
|
||||||
|
|
||||||
|
// executeMacro is used to execute a macro consisting of multiple steps.
|
||||||
|
// Each step can have multiple keys, multiple modifiers and a delay.
|
||||||
|
// The keys and modifiers are pressed together and held for the delay duration.
|
||||||
|
// After the delay, the keys and modifiers are released and the next step is executed.
|
||||||
|
// If a step has no keys or modifiers, it is treated as a delay-only step.
|
||||||
|
// A small pause is added between steps to ensure that the device can process the events.
|
||||||
const executeMacro = async (steps: { keys: string[] | null; modifiers: string[] | null; delay: number }[]) => {
|
const executeMacro = async (steps: { keys: string[] | null; modifiers: string[] | null; delay: number }[]) => {
|
||||||
for (const [index, step] of steps.entries()) {
|
for (const [index, step] of steps.entries()) {
|
||||||
const keyValues = step.keys?.map(key => keys[key]).filter(Boolean) || [];
|
const keyValues = (step.keys || []).map(key => keys[key]).filter(Boolean);
|
||||||
const modifierValues = step.modifiers?.map(mod => modifiers[mod]).filter(Boolean) || [];
|
const modifierMask: number = (step.modifiers || []).map(mod => modifiers[mod]).reduce((acc, val) => acc + val, 0);
|
||||||
|
|
||||||
// If the step has keys and/or modifiers, press them and hold for the delay
|
// If the step has keys and/or modifiers, press them and hold for the delay
|
||||||
if (keyValues.length > 0 || modifierValues.length > 0) {
|
if (keyValues.length > 0 || modifierMask > 0) {
|
||||||
sendKeyboardEvent(keyValues, modifierValues);
|
sendKeyboardEvent({ keys: keyValues, modifier: modifierMask });
|
||||||
await new Promise(resolve => setTimeout(resolve, step.delay || 50));
|
await new Promise(resolve => setTimeout(resolve, step.delay || 50));
|
||||||
|
|
||||||
resetKeyboardState();
|
resetKeyboardState();
|
||||||
|
@ -52,5 +127,92 @@ export default function useKeyboard() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return { sendKeyboardEvent, resetKeyboardState, executeMacro };
|
// handleKeyPress is used to handle a key press or release event.
|
||||||
|
// This function handle both key press and key release events.
|
||||||
|
// It checks if the keyPressReport API is available and sends the key press event.
|
||||||
|
// If the keyPressReport API is not available, it simulates the device-side key
|
||||||
|
// handling for legacy devices and updates the keysDownState accordingly.
|
||||||
|
// It then sends the full keyboard state to the device.
|
||||||
|
const handleKeyPress = useCallback(
|
||||||
|
async (key: number, press: boolean) => {
|
||||||
|
if (rpcDataChannel?.readyState !== "open") return;
|
||||||
|
if ((key || 0) === 0) return; // ignore zero key presses (they are bad mappings)
|
||||||
|
|
||||||
|
if (keyPressReportApiAvailable) {
|
||||||
|
// if the keyPress api is available, we can just send the key press event
|
||||||
|
sendKeypressEvent(key, press);
|
||||||
|
} else {
|
||||||
|
// if the keyPress api is not available, we need to handle the key locally
|
||||||
|
const downState = simulateDeviceSideKeyHandlingForLegacyDevices(keysDownState, key, press);
|
||||||
|
sendKeyboardEvent(downState); // then we send the full state
|
||||||
|
|
||||||
|
// if we just sent ErrorRollOver, reset to empty state
|
||||||
|
if (downState.keys[0] === hidErrorRollOver) {
|
||||||
|
resetKeyboardState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[keyPressReportApiAvailable, keysDownState, resetKeyboardState, rpcDataChannel?.readyState, sendKeyboardEvent, sendKeypressEvent],
|
||||||
|
);
|
||||||
|
|
||||||
|
// IMPORTANT: See the keyPressReportApiAvailable comment above for the reason this exists
|
||||||
|
function simulateDeviceSideKeyHandlingForLegacyDevices(state: KeysDownState, key: number, press: boolean): KeysDownState {
|
||||||
|
// IMPORTANT: This code parallels the logic in the kernel's hid-gadget driver
|
||||||
|
// for handling key presses and releases. It ensures that the USB gadget
|
||||||
|
// behaves similarly to a real USB HID keyboard. This logic is paralleled
|
||||||
|
// in the device-side code in hid_keyboard.go so make sure to keep them in sync.
|
||||||
|
let modifiers = state.modifier;
|
||||||
|
const keys = state.keys;
|
||||||
|
const modifierMask = hidKeyToModifierMask[key] || 0;
|
||||||
|
|
||||||
|
if (modifierMask !== 0) {
|
||||||
|
// If the key is a modifier key, we update the keyboardModifier state
|
||||||
|
// by setting or clearing the corresponding bit in the modifier byte.
|
||||||
|
// This allows us to track the state of dynamic modifier keys like
|
||||||
|
// Shift, Control, Alt, and Super.
|
||||||
|
if (press) {
|
||||||
|
modifiers |= modifierMask;
|
||||||
|
} else {
|
||||||
|
modifiers &= ~modifierMask;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// handle other keys that are not modifier keys by placing or removing them
|
||||||
|
// from the key buffer since the buffer tracks currently pressed keys
|
||||||
|
let overrun = true;
|
||||||
|
for (let i = 0; i < hidKeyBufferSize; i++) {
|
||||||
|
// If we find the key in the buffer the buffer, we either remove it (if press is false)
|
||||||
|
// or do nothing (if down is true) because the buffer tracks currently pressed keys
|
||||||
|
// and if we find a zero byte, we can place the key there (if press is true)
|
||||||
|
if (keys[i] === key || keys[i] === 0) {
|
||||||
|
if (press) {
|
||||||
|
keys[i] = key // overwrites the zero byte or the same key if already pressed
|
||||||
|
} else {
|
||||||
|
// we are releasing the key, remove it from the buffer
|
||||||
|
if (keys[i] !== 0) {
|
||||||
|
keys.splice(i, 1);
|
||||||
|
keys.push(0); // add a zero at the end
|
||||||
|
}
|
||||||
|
}
|
||||||
|
overrun = false; // We found a slot for the key
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we reach here it means we didn't find an empty slot or the key in the buffer
|
||||||
|
if (overrun) {
|
||||||
|
if (press) {
|
||||||
|
console.warn(`keyboard buffer overflow current keys ${keys}, key: ${key} not added`);
|
||||||
|
// Fill all key slots with ErrorRollOver (0x01) to indicate overflow
|
||||||
|
keys.length = hidKeyBufferSize;
|
||||||
|
keys.fill(hidErrorRollOver);
|
||||||
|
} else {
|
||||||
|
// If we are releasing a key, and we didn't find it in a slot, who cares?
|
||||||
|
console.debug(`key ${key} not found in buffer, nothing to release`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { modifier: modifiers, keys };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { handleKeyPress, resetKeyboardState, executeMacro };
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
|
import { useSettingsStore } from "@/hooks/stores";
|
||||||
|
import { keyboards } from "@/keyboardLayouts";
|
||||||
|
|
||||||
|
export default function useKeyboardLayout() {
|
||||||
|
const { keyboardLayout } = useSettingsStore();
|
||||||
|
|
||||||
|
const keyboardOptions = useMemo(() => {
|
||||||
|
return keyboards.map((keyboard) => {
|
||||||
|
return { label: keyboard.name, value: keyboard.isoCode }
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const isoCode = useMemo(() => {
|
||||||
|
// If we don't have a specific layout, default to "en-US" because that was the original layout
|
||||||
|
// developed so it is a good fallback. Additionally, we replace "en_US" with "en-US" because
|
||||||
|
// the original server-side code used "en_US" as the default value, but that's not the correct
|
||||||
|
// ISO code for English/United State. To ensure we remain backward compatible with devices that
|
||||||
|
// have not had their Keyboard Layout selected by the user, we want to treat "en_US" as if it was
|
||||||
|
// "en-US" to match the ISO standard codes now used in the keyboardLayouts.
|
||||||
|
console.debug("Current keyboard layout from store:", keyboardLayout);
|
||||||
|
if (keyboardLayout && keyboardLayout.length > 0)
|
||||||
|
return keyboardLayout.replace("en_US", "en-US");
|
||||||
|
return "en-US";
|
||||||
|
}, [keyboardLayout]);
|
||||||
|
|
||||||
|
const selectedKeyboard = useMemo(() => {
|
||||||
|
// fallback to original behaviour of en-US if no isoCode given or matching layout not found
|
||||||
|
return keyboards.find(keyboard => keyboard.isoCode === isoCode)
|
||||||
|
?? keyboards.find(keyboard => keyboard.isoCode === "en-US")!;
|
||||||
|
}, [isoCode]);
|
||||||
|
|
||||||
|
return { keyboardOptions, isoCode, selectedKeyboard };
|
||||||
|
}
|
|
@ -315,6 +315,11 @@ video::-webkit-media-controls {
|
||||||
@apply inline-flex h-auto! w-auto! grow-0 py-1 text-xs;
|
@apply inline-flex h-auto! w-auto! grow-0 py-1 text-xs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hg-theme-default .hg-row .down-key {
|
||||||
|
background: rgb(28, 28, 28);
|
||||||
|
@apply text-white! font-bold!;
|
||||||
|
}
|
||||||
|
|
||||||
.hg-theme-default .hg-row .hg-button-container,
|
.hg-theme-default .hg-row .hg-button-container,
|
||||||
.hg-theme-default .hg-row .hg-button:not(:last-child) {
|
.hg-theme-default .hg-row .hg-button:not(:last-child) {
|
||||||
@apply mr-[2px]! md:mr-[5px]!;
|
@apply mr-[2px]! md:mr-[5px]!;
|
||||||
|
|
|
@ -1,9 +1,20 @@
|
||||||
export interface KeyStroke { modifier: number; keys: number[]; }
|
export interface KeyStroke { modifier: number; keys: number[]; }
|
||||||
export interface KeyInfo { key: string | number; shift?: boolean, altRight?: boolean }
|
export interface KeyInfo { key: string | number; shift?: boolean, altRight?: boolean }
|
||||||
export interface KeyCombo extends KeyInfo { deadKey?: boolean, accentKey?: KeyInfo }
|
export interface KeyCombo extends KeyInfo { deadKey?: boolean, accentKey?: KeyInfo }
|
||||||
export interface KeyboardLayout { isoCode: string, name: string, chars: Record<string, KeyCombo> }
|
export interface KeyboardLayout {
|
||||||
|
isoCode: string;
|
||||||
|
name: string;
|
||||||
|
chars: Record<string, KeyCombo>;
|
||||||
|
modifierDisplayMap: Record<string, string>;
|
||||||
|
keyDisplayMap: Record<string, string>;
|
||||||
|
virtualKeyboard: {
|
||||||
|
main: { default: string[], shift: string[] },
|
||||||
|
control?: { default: string[], shift?: string[] },
|
||||||
|
arrows?: { default: string[] }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// to add a new layout, create a file like the above and add it to the list
|
// 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 { cs_CZ } from "@/keyboardLayouts/cs_CZ"
|
||||||
import { de_CH } from "@/keyboardLayouts/de_CH"
|
import { de_CH } from "@/keyboardLayouts/de_CH"
|
||||||
import { de_DE } from "@/keyboardLayouts/de_DE"
|
import { de_DE } from "@/keyboardLayouts/de_DE"
|
||||||
|
@ -18,15 +29,3 @@ import { nb_NO } from "@/keyboardLayouts/nb_NO"
|
||||||
import { sv_SE } from "@/keyboardLayouts/sv_SE"
|
import { sv_SE } from "@/keyboardLayouts/sv_SE"
|
||||||
|
|
||||||
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 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 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,17 +1,20 @@
|
||||||
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
|
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
|
||||||
|
|
||||||
const name = "Čeština";
|
import { en_US } from "./en_US" // for fallback of keyDisplayMap, modifierDisplayMap, and virtualKeyboard
|
||||||
|
|
||||||
const keyTrema = { key: "Backslash" } // tréma (umlaut), two dots placed above a vowel
|
const name = "Čeština";
|
||||||
const keyAcute = { key: "Equal" } // accent aigu (acute accent), mark ´ placed above the letter
|
const isoCode = "cs-CZ";
|
||||||
const keyHat = { key: "Digit3", shift: true, altRight: true } // accent circonflexe (accent hat), mark ^ placed above the letter
|
|
||||||
const keyCaron = { key: "Equal", shift: true } // caron or haček (inverted hat), mark ˇ placed above the letter
|
const keyTrema: KeyCombo = { key: "Backslash" } // tréma (umlaut), two dots placed above a vowel
|
||||||
const keyGrave = { key: "Digit7", shift: true, altRight: true } // accent grave, mark ` placed above the letter
|
const keyAcute: KeyCombo = { key: "Equal" } // accent aigu (acute accent), mark ´ placed above the letter
|
||||||
const keyTilde = { key: "Digit1", shift: true, altRight: true } // tilde, mark ~ placed above the letter
|
const keyHat: KeyCombo = { key: "Digit3", shift: true, altRight: true } // accent circonflexe (accent hat), mark ^ placed above the letter
|
||||||
const keyRing = { key: "Backquote", shift: true } // kroužek (little ring), mark ° placed above the letter
|
const keyCaron: KeyCombo = { key: "Equal", shift: true } // caron or haček (inverted hat), mark ˇ placed above the letter
|
||||||
const keyOverdot = { key: "Digit8", shift: true, altRight: true } // overdot (dot above), mark ˙ placed above the letter
|
const keyGrave: KeyCombo = { key: "Digit7", shift: true, altRight: true } // accent grave, mark ` placed above the letter
|
||||||
const keyHook = { key: "Digit6", shift: true, altRight: true } // ogonoek (little hook), mark ˛ placed beneath a letter
|
const keyTilde: KeyCombo = { key: "Digit1", shift: true, altRight: true } // tilde, mark ~ placed above the letter
|
||||||
const keyCedille = { key: "Equal", shift: true, altRight: true } // accent cedille (cedilla), mark ¸ placed beneath a letter
|
const keyRing: KeyCombo = { key: "Backquote", shift: true } // kroužek (little ring), mark ° placed above the letter
|
||||||
|
const keyOverdot: KeyCombo = { key: "Digit8", shift: true, altRight: true } // overdot (dot above), mark ˙ placed above the letter
|
||||||
|
const keyHook: KeyCombo = { key: "Digit6", shift: true, altRight: true } // ogonoek (little hook), mark ˛ placed beneath a letter
|
||||||
|
const keyCedille: KeyCombo = { key: "Equal", shift: true, altRight: true } // accent cedille (cedilla), mark ¸ placed beneath a letter
|
||||||
|
|
||||||
const chars = {
|
const chars = {
|
||||||
A: { key: "KeyA", shift: true },
|
A: { key: "KeyA", shift: true },
|
||||||
|
@ -244,7 +247,11 @@ const chars = {
|
||||||
} as Record<string, KeyCombo>;
|
} as Record<string, KeyCombo>;
|
||||||
|
|
||||||
export const cs_CZ: KeyboardLayout = {
|
export const cs_CZ: KeyboardLayout = {
|
||||||
isoCode: "cs-CZ",
|
isoCode: isoCode,
|
||||||
name: name,
|
name: name,
|
||||||
chars: chars
|
chars: chars,
|
||||||
|
// TODO need to localize these maps and layouts
|
||||||
|
keyDisplayMap: en_US.keyDisplayMap,
|
||||||
|
modifierDisplayMap: en_US.modifierDisplayMap,
|
||||||
|
virtualKeyboard: en_US.virtualKeyboard
|
||||||
};
|
};
|
|
@ -1,12 +1,15 @@
|
||||||
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
|
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
|
||||||
|
|
||||||
const name = "Schwiizerdütsch";
|
import { en_US } from "./en_US" // for fallback of keyDisplayMap, modifierDisplayMap, and virtualKeyboard
|
||||||
|
|
||||||
const keyTrema = { key: "BracketRight" } // tréma (umlaut), two dots placed above a vowel
|
const name = "Schwiizerdütsch";
|
||||||
const keyAcute = { key: "Minus", altRight: true } // accent aigu (acute accent), mark ´ placed above the letter
|
const isoCode = "de-CH";
|
||||||
const keyHat = { key: "Equal" } // accent circonflexe (accent hat), mark ^ placed above the letter
|
|
||||||
const keyGrave = { key: "Equal", shift: true } // accent grave, mark ` placed above the letter
|
const keyTrema: KeyCombo = { key: "BracketRight" } // tréma (umlaut), two dots placed above a vowel
|
||||||
const keyTilde = { key: "Equal", altRight: true } // tilde, mark ~ placed above the letter
|
const keyAcute: KeyCombo = { key: "Minus", altRight: true } // accent aigu (acute accent), mark ´ placed above the letter
|
||||||
|
const keyHat: KeyCombo = { key: "Equal" } // accent circonflexe (accent hat), mark ^ placed above the letter
|
||||||
|
const keyGrave: KeyCombo = { key: "Equal", shift: true } // accent grave, mark ` placed above the letter
|
||||||
|
const keyTilde: KeyCombo = { key: "Equal", altRight: true } // tilde, mark ~ placed above the letter
|
||||||
|
|
||||||
const chars = {
|
const chars = {
|
||||||
A: { key: "KeyA", shift: true },
|
A: { key: "KeyA", shift: true },
|
||||||
|
@ -164,8 +167,22 @@ const chars = {
|
||||||
Tab: { key: "Tab" },
|
Tab: { key: "Tab" },
|
||||||
} as Record<string, KeyCombo>;
|
} as Record<string, KeyCombo>;
|
||||||
|
|
||||||
|
const keyDisplayMap = {
|
||||||
|
...en_US.keyDisplayMap,
|
||||||
|
BracketLeft: "è",
|
||||||
|
"(BracketLeft)": "ü",
|
||||||
|
Semicolon: "é",
|
||||||
|
"(Semicolon)": "ö",
|
||||||
|
Quote: "à",
|
||||||
|
"(Quote)": "ä",
|
||||||
|
} as Record<string, string>;
|
||||||
|
|
||||||
export const de_CH: KeyboardLayout = {
|
export const de_CH: KeyboardLayout = {
|
||||||
isoCode: "de-CH",
|
isoCode: isoCode,
|
||||||
name: name,
|
name: name,
|
||||||
chars: chars
|
chars: chars,
|
||||||
|
keyDisplayMap: keyDisplayMap,
|
||||||
|
// TODO need to localize these maps and layouts
|
||||||
|
modifierDisplayMap: en_US.modifierDisplayMap,
|
||||||
|
virtualKeyboard: en_US.virtualKeyboard
|
||||||
};
|
};
|
|
@ -1,113 +1,146 @@
|
||||||
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
|
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
|
||||||
|
|
||||||
const name = "Deutsch";
|
import { en_US } from "./en_US" // for fallback of keyDisplayMap, modifierDisplayMap, and virtualKeyboard
|
||||||
|
|
||||||
const keyAcute = { key: "Equal" } // accent aigu (acute accent), mark ´ placed above the letter
|
const name = "Deutsch";
|
||||||
const keyHat = { key: "Backquote" } // accent circonflexe (accent hat), mark ^ placed above the letter
|
const isoCode = "de-DE";
|
||||||
const keyGrave = { key: "Equal", shift: true } // accent grave, mark ` placed above the letter
|
|
||||||
|
const keyAcute: KeyCombo = { key: "Equal" } // accent aigu (acute accent), mark ´ placed above the letter
|
||||||
|
const keyHat: KeyCombo = { key: "Backquote" } // accent circonflexe (accent hat), mark ^ placed above the letter
|
||||||
|
const keyGrave: KeyCombo = { key: "Equal", shift: true } // accent grave, mark ` placed above the letter
|
||||||
|
|
||||||
const chars = {
|
const chars = {
|
||||||
|
a: { key: "KeyA" },
|
||||||
|
"á": { key: "KeyA", accentKey: keyAcute },
|
||||||
|
"â": { key: "KeyA", accentKey: keyHat },
|
||||||
|
"à": { key: "KeyA", accentKey: keyGrave },
|
||||||
A: { key: "KeyA", shift: true },
|
A: { key: "KeyA", shift: true },
|
||||||
"Á": { key: "KeyA", shift: true, accentKey: keyAcute },
|
"Á": { key: "KeyA", shift: true, accentKey: keyAcute },
|
||||||
"Â": { key: "KeyA", shift: true, accentKey: keyHat },
|
"Â": { key: "KeyA", shift: true, accentKey: keyHat },
|
||||||
"À": { key: "KeyA", shift: true, accentKey: keyGrave },
|
"À": { key: "KeyA", shift: true, accentKey: keyGrave },
|
||||||
|
"☺": { key: "KeyA", altRight: true }, // white smiling face ☺
|
||||||
|
b: { key: "KeyB" },
|
||||||
B: { key: "KeyB", shift: true },
|
B: { key: "KeyB", shift: true },
|
||||||
|
"‹": { key: "KeyB", altRight: true }, // single left-pointing angle quotation mark, ‹
|
||||||
|
c: { key: "KeyC" },
|
||||||
C: { key: "KeyC", shift: true },
|
C: { key: "KeyC", shift: true },
|
||||||
|
"\u202f": { key: "KeyC", altRight: true }, // narrow no-break space
|
||||||
|
d: { key: "KeyD" },
|
||||||
D: { key: "KeyD", shift: true },
|
D: { key: "KeyD", shift: true },
|
||||||
|
"′": { key: "KeyD", altRight: true }, // prime, mark ′ placed above the letter
|
||||||
|
e: { key: "KeyE" },
|
||||||
|
"é": { key: "KeyE", accentKey: keyAcute },
|
||||||
|
"ê": { key: "KeyE", accentKey: keyHat },
|
||||||
|
"è": { key: "KeyE", accentKey: keyGrave },
|
||||||
|
"€": { key: "KeyE", altRight: true },
|
||||||
E: { key: "KeyE", shift: true },
|
E: { key: "KeyE", shift: true },
|
||||||
"É": { key: "KeyE", shift: true, accentKey: keyAcute },
|
"É": { key: "KeyE", shift: true, accentKey: keyAcute },
|
||||||
"Ê": { key: "KeyE", shift: true, accentKey: keyHat },
|
"Ê": { key: "KeyE", shift: true, accentKey: keyHat },
|
||||||
"È": { key: "KeyE", shift: true, accentKey: keyGrave },
|
"È": { key: "KeyE", shift: true, accentKey: keyGrave },
|
||||||
F: { key: "KeyF", shift: true },
|
|
||||||
G: { key: "KeyG", shift: true },
|
|
||||||
H: { key: "KeyH", shift: true },
|
|
||||||
I: { key: "KeyI", shift: true },
|
|
||||||
"Í": { key: "KeyI", shift: true, accentKey: keyAcute },
|
|
||||||
"Î": { key: "KeyI", shift: true, accentKey: keyHat },
|
|
||||||
"Ì": { key: "KeyI", shift: true, accentKey: keyGrave },
|
|
||||||
J: { key: "KeyJ", shift: true },
|
|
||||||
K: { key: "KeyK", shift: true },
|
|
||||||
L: { key: "KeyL", shift: true },
|
|
||||||
M: { key: "KeyM", shift: true },
|
|
||||||
N: { key: "KeyN", shift: true },
|
|
||||||
O: { key: "KeyO", shift: true },
|
|
||||||
"Ó": { key: "KeyO", shift: true, accentKey: keyAcute },
|
|
||||||
"Ô": { key: "KeyO", shift: true, accentKey: keyHat },
|
|
||||||
"Ò": { key: "KeyO", shift: true, accentKey: keyGrave },
|
|
||||||
P: { key: "KeyP", shift: true },
|
|
||||||
Q: { key: "KeyQ", shift: true },
|
|
||||||
R: { key: "KeyR", shift: true },
|
|
||||||
S: { key: "KeyS", shift: true },
|
|
||||||
T: { key: "KeyT", shift: true },
|
|
||||||
U: { key: "KeyU", shift: true },
|
|
||||||
"Ú": { key: "KeyU", shift: true, accentKey: keyAcute },
|
|
||||||
"Û": { key: "KeyU", shift: true, accentKey: keyHat },
|
|
||||||
"Ù": { key: "KeyU", shift: true, accentKey: keyGrave },
|
|
||||||
V: { key: "KeyV", shift: true },
|
|
||||||
W: { key: "KeyW", shift: true },
|
|
||||||
X: { key: "KeyX", shift: true },
|
|
||||||
Y: { key: "KeyZ", shift: true },
|
|
||||||
Z: { key: "KeyY", shift: true },
|
|
||||||
a: { key: "KeyA" },
|
|
||||||
"á": { key: "KeyA", accentKey: keyAcute },
|
|
||||||
"â": { key: "KeyA", accentKey: keyHat },
|
|
||||||
"à": { key: "KeyA", accentKey: keyGrave},
|
|
||||||
b: { key: "KeyB" },
|
|
||||||
c: { key: "KeyC" },
|
|
||||||
d: { key: "KeyD" },
|
|
||||||
e: { key: "KeyE" },
|
|
||||||
"é": { key: "KeyE", accentKey: keyAcute},
|
|
||||||
"ê": { key: "KeyE", accentKey: keyHat },
|
|
||||||
"è": { key: "KeyE", accentKey: keyGrave },
|
|
||||||
"€": { key: "KeyE", altRight: true },
|
|
||||||
f: { key: "KeyF" },
|
f: { key: "KeyF" },
|
||||||
|
F: { key: "KeyF", shift: true },
|
||||||
|
"˟": { key: "KeyF", deadKey: true, altRight: true }, // modifier letter cross accent, ˟
|
||||||
|
G: { key: "KeyG", shift: true },
|
||||||
g: { key: "KeyG" },
|
g: { key: "KeyG" },
|
||||||
|
"ẞ": { key: "KeyG", altRight: true }, // capital sharp S, ẞ
|
||||||
h: { key: "KeyH" },
|
h: { key: "KeyH" },
|
||||||
|
H: { key: "KeyH", shift: true },
|
||||||
|
"ˍ": { key: "KeyH", deadKey: true, altRight: true }, // modifier letter low macron, ˍ
|
||||||
i: { key: "KeyI" },
|
i: { key: "KeyI" },
|
||||||
"í": { key: "KeyI", accentKey: keyAcute },
|
"í": { key: "KeyI", accentKey: keyAcute },
|
||||||
"î": { key: "KeyI", accentKey: keyHat },
|
"î": { key: "KeyI", accentKey: keyHat },
|
||||||
"ì": { key: "KeyI", accentKey: keyGrave },
|
"ì": { key: "KeyI", accentKey: keyGrave },
|
||||||
|
I: { key: "KeyI", shift: true },
|
||||||
|
"Í": { key: "KeyI", shift: true, accentKey: keyAcute },
|
||||||
|
"Î": { key: "KeyI", shift: true, accentKey: keyHat },
|
||||||
|
"Ì": { key: "KeyI", shift: true, accentKey: keyGrave },
|
||||||
|
"˜": { key: "KeyI", deadKey: true, altRight: true }, // tilde accent, mark ˜ placed above the letter
|
||||||
j: { key: "KeyJ" },
|
j: { key: "KeyJ" },
|
||||||
|
J: { key: "KeyJ", shift: true },
|
||||||
|
"¸": { key: "KeyJ", deadKey: true, altRight: true }, // cedilla accent, mark ¸ placed below the letter
|
||||||
k: { key: "KeyK" },
|
k: { key: "KeyK" },
|
||||||
|
K: { key: "KeyK", shift: true },
|
||||||
l: { key: "KeyL" },
|
l: { key: "KeyL" },
|
||||||
|
L: { key: "KeyL", shift: true },
|
||||||
|
"ˏ": { key: "KeyL", deadKey: true, altRight: true }, // modifier letter reversed comma, ˏ
|
||||||
m: { key: "KeyM" },
|
m: { key: "KeyM" },
|
||||||
|
M: { key: "KeyM", shift: true },
|
||||||
"µ": { key: "KeyM", altRight: true },
|
"µ": { key: "KeyM", altRight: true },
|
||||||
n: { key: "KeyN" },
|
n: { key: "KeyN" },
|
||||||
|
N: { key: "KeyN", shift: true },
|
||||||
|
"–": { key: "KeyN", altRight: true }, // en dash, –
|
||||||
o: { key: "KeyO" },
|
o: { key: "KeyO" },
|
||||||
"ó": { key: "KeyO", accentKey: keyAcute },
|
"ó": { key: "KeyO", accentKey: keyAcute },
|
||||||
"ô": { key: "KeyO", accentKey: keyHat },
|
"ô": { key: "KeyO", accentKey: keyHat },
|
||||||
"ò": { key: "KeyO", accentKey: keyGrave },
|
"ò": { key: "KeyO", accentKey: keyGrave },
|
||||||
|
O: { key: "KeyO", shift: true },
|
||||||
|
"Ó": { key: "KeyO", shift: true, accentKey: keyAcute },
|
||||||
|
"Ô": { key: "KeyO", shift: true, accentKey: keyHat },
|
||||||
|
"Ò": { key: "KeyO", shift: true, accentKey: keyGrave },
|
||||||
|
"˚": { key: "KeyO", deadKey: true, altRight: true }, // ring above, ˚
|
||||||
p: { key: "KeyP" },
|
p: { key: "KeyP" },
|
||||||
|
P: { key: "KeyP", shift: true },
|
||||||
|
"ˀ": { key: "KeyP", deadKey: true, altRight: true }, // modifier letter apostrophe, ʾ
|
||||||
q: { key: "KeyQ" },
|
q: { key: "KeyQ" },
|
||||||
|
Q: { key: "KeyQ", shift: true },
|
||||||
"@": { key: "KeyQ", altRight: true },
|
"@": { key: "KeyQ", altRight: true },
|
||||||
|
R: { key: "KeyR", shift: true },
|
||||||
r: { key: "KeyR" },
|
r: { key: "KeyR" },
|
||||||
|
"˝": { key: "KeyR", deadKey: true, altRight: true }, // double acute accent, mark ˝ placed above the letter
|
||||||
|
S: { key: "KeyS", shift: true },
|
||||||
s: { key: "KeyS" },
|
s: { key: "KeyS" },
|
||||||
|
"″": { key: "KeyS", altRight: true }, // double prime, mark ″ placed above the letter
|
||||||
|
T: { key: "KeyT", shift: true },
|
||||||
t: { key: "KeyT" },
|
t: { key: "KeyT" },
|
||||||
|
"ˇ": { key: "KeyT", deadKey: true, altRight: true }, // caron/hacek accent, mark ˇ placed above the letter
|
||||||
u: { key: "KeyU" },
|
u: { key: "KeyU" },
|
||||||
"ú": { key: "KeyU", accentKey: keyAcute },
|
"ú": { key: "KeyU", accentKey: keyAcute },
|
||||||
"û": { key: "KeyU", accentKey: keyHat },
|
"û": { key: "KeyU", accentKey: keyHat },
|
||||||
"ù": { key: "KeyU", accentKey: keyGrave },
|
"ù": { key: "KeyU", accentKey: keyGrave },
|
||||||
|
U: { key: "KeyU", shift: true },
|
||||||
|
"Ú": { key: "KeyU", shift: true, accentKey: keyAcute },
|
||||||
|
"Û": { key: "KeyU", shift: true, accentKey: keyHat },
|
||||||
|
"Ù": { key: "KeyU", shift: true, accentKey: keyGrave },
|
||||||
|
"˘": { key: "KeyU", deadKey: true, altRight: true }, // breve accent, ˘ placed above the letter
|
||||||
v: { key: "KeyV" },
|
v: { key: "KeyV" },
|
||||||
|
V: { key: "KeyV", shift: true },
|
||||||
|
"«": { key: "KeyV", altRight: true }, // left-pointing double angle quotation mark, «
|
||||||
w: { key: "KeyW" },
|
w: { key: "KeyW" },
|
||||||
|
W: { key: "KeyW", shift: true },
|
||||||
|
"¯": { key: "KeyW", deadKey: true, altRight: true }, // macron accent, mark ¯ placed above the letter
|
||||||
x: { key: "KeyX" },
|
x: { key: "KeyX" },
|
||||||
|
X: { key: "KeyX", shift: true },
|
||||||
|
"»": { key: "KeyX", altRight: true },
|
||||||
|
// cross key between shift and y (aka OEM 102 key)
|
||||||
y: { key: "KeyZ" },
|
y: { key: "KeyZ" },
|
||||||
|
Y: { key: "KeyZ", shift: true },
|
||||||
|
"›": { key: "KeyZ", altRight: true }, // single right-pointing angle quotation mark, ›
|
||||||
z: { key: "KeyY" },
|
z: { key: "KeyY" },
|
||||||
|
Z: { key: "KeyY", shift: true },
|
||||||
|
"¨": { key: "KeyY", deadKey: true, altRight: true }, // diaeresis accent, mark ¨ placed above the letter
|
||||||
"°": { key: "Backquote", shift: true },
|
"°": { key: "Backquote", shift: true },
|
||||||
"^": { key: "Backquote", deadKey: true },
|
"^": { key: "Backquote", deadKey: true },
|
||||||
|
"|": { key: "Backquote", altRight: true },
|
||||||
1: { key: "Digit1" },
|
1: { key: "Digit1" },
|
||||||
"!": { key: "Digit1", shift: true },
|
"!": { key: "Digit1", shift: true },
|
||||||
|
"’": { key: "Digit1", altRight: true }, // single quote, mark ’ placed above the letter
|
||||||
2: { key: "Digit2" },
|
2: { key: "Digit2" },
|
||||||
"\"": { key: "Digit2", shift: true },
|
"\"": { key: "Digit2", shift: true },
|
||||||
"²": { key: "Digit2", altRight: true },
|
"²": { key: "Digit2", altRight: true },
|
||||||
|
"<": { key: "Digit2", altRight: true }, // non-US < and >
|
||||||
3: { key: "Digit3" },
|
3: { key: "Digit3" },
|
||||||
"§": { key: "Digit3", shift: true },
|
"§": { key: "Digit3", shift: true },
|
||||||
"³": { key: "Digit3", altRight: true },
|
"³": { key: "Digit3", altRight: true },
|
||||||
|
">": { key: "Digit3", altRight: true }, // non-US < and >
|
||||||
4: { key: "Digit4" },
|
4: { key: "Digit4" },
|
||||||
"$": { key: "Digit4", shift: true },
|
"$": { key: "Digit4", shift: true },
|
||||||
|
"—": { key: "Digit4", altRight: true }, // em dash, —
|
||||||
5: { key: "Digit5" },
|
5: { key: "Digit5" },
|
||||||
"%": { key: "Digit5", shift: true },
|
"%": { key: "Digit5", shift: true },
|
||||||
|
"¡": { key: "Digit5", altRight: true }, // inverted exclamation mark, ¡
|
||||||
6: { key: "Digit6" },
|
6: { key: "Digit6" },
|
||||||
"&": { key: "Digit6", shift: true },
|
"&": { key: "Digit6", shift: true },
|
||||||
|
"¿": { key: "Digit6", altRight: true }, // inverted question mark, ¿
|
||||||
7: { key: "Digit7" },
|
7: { key: "Digit7" },
|
||||||
"/": { key: "Digit7", shift: true },
|
"/": { key: "Digit7", shift: true },
|
||||||
"{": { key: "Digit7", altRight: true },
|
"{": { key: "Digit7", altRight: true },
|
||||||
|
@ -123,36 +156,192 @@ const chars = {
|
||||||
"ß": { key: "Minus" },
|
"ß": { key: "Minus" },
|
||||||
"?": { key: "Minus", shift: true },
|
"?": { key: "Minus", shift: true },
|
||||||
"\\": { key: "Minus", altRight: true },
|
"\\": { key: "Minus", altRight: true },
|
||||||
"´": { key: "Equal", deadKey: true },
|
"´": { key: "Equal", deadKey: true }, // accent acute, mark ´ placed above the letter
|
||||||
"`": { key: "Equal", shift: true, deadKey: true },
|
"`": { key: "Equal", shift: true, deadKey: true }, // accent grave, mark ` placed above the letter
|
||||||
|
"˙": { key: "Equal", control: true, altRight: true, deadKey: true }, // acute accent, mark ˙ placed above the letter
|
||||||
"ü": { key: "BracketLeft" },
|
"ü": { key: "BracketLeft" },
|
||||||
"Ü": { key: "BracketLeft", shift: true },
|
"Ü": { key: "BracketLeft", shift: true },
|
||||||
|
Escape: { key: "BracketLeft", control: true },
|
||||||
|
"ʼ": { key: "BracketLeft", altRight: true }, // modifier letter apostrophe, ʼ
|
||||||
"+": { key: "BracketRight" },
|
"+": { key: "BracketRight" },
|
||||||
"*": { key: "BracketRight", shift: true },
|
"*": { key: "BracketRight", shift: true },
|
||||||
|
Control: { key: "BracketRight", control: true },
|
||||||
"~": { key: "BracketRight", altRight: true },
|
"~": { key: "BracketRight", altRight: true },
|
||||||
"ö": { key: "Semicolon" },
|
"ö": { key: "Semicolon" },
|
||||||
"Ö": { key: "Semicolon", shift: true },
|
"Ö": { key: "Semicolon", shift: true },
|
||||||
|
"ˌ": { key: "Semicolon", deadkey: true, altRight: true }, // modifier letter low vertical line, ˌ
|
||||||
"ä": { key: "Quote" },
|
"ä": { key: "Quote" },
|
||||||
"Ä": { key: "Quote", shift: true },
|
"Ä": { key: "Quote", shift: true },
|
||||||
|
"˗": { key: "Quote", deadKey: true, altRight: true }, // modifier letter minus sign, ˗
|
||||||
"#": { key: "Backslash" },
|
"#": { key: "Backslash" },
|
||||||
"'": { key: "Backslash", shift: true },
|
"'": { key: "Backslash", shift: true },
|
||||||
|
"−": { key: "Backslash", altRight: true }, // minus sign, −
|
||||||
",": { key: "Comma" },
|
",": { key: "Comma" },
|
||||||
";": { key: "Comma", shift: true },
|
";": { key: "Comma", shift: true },
|
||||||
|
"\u2011": { key: "Comma", altRight: true }, // non-breaking hyphen, ‑
|
||||||
".": { key: "Period" },
|
".": { key: "Period" },
|
||||||
":": { key: "Period", shift: true },
|
":": { key: "Period", shift: true },
|
||||||
|
"·": { key: "Period", altRight: true }, // middle dot, ·
|
||||||
"-": { key: "Slash" },
|
"-": { key: "Slash" },
|
||||||
"_": { key: "Slash", shift: true },
|
"_": { key: "Slash", shift: true },
|
||||||
"<": { key: "IntlBackslash" },
|
"\u00ad": { key: "Slash", altRight: true }, // soft hyphen,
|
||||||
">": { key: "IntlBackslash", shift: true },
|
|
||||||
"|": { key: "IntlBackslash", altRight: true },
|
|
||||||
" ": { key: "Space" },
|
" ": { key: "Space" },
|
||||||
"\n": { key: "Enter" },
|
"\n": { key: "Enter" },
|
||||||
Enter: { key: "Enter" },
|
Enter: { key: "Enter" },
|
||||||
Tab: { key: "Tab" },
|
Tab: { key: "Tab" },
|
||||||
} as Record<string, KeyCombo>;
|
} as Record<string, KeyCombo>;
|
||||||
|
|
||||||
|
export const keyDisplayMap: Record<string, string> = {
|
||||||
|
...en_US.keyDisplayMap,
|
||||||
|
// now override the English keyDisplayMap with German specific keys
|
||||||
|
|
||||||
|
// Combination keys
|
||||||
|
CtrlAltDelete: "Strg + Alt + Entf",
|
||||||
|
CtrlAltBackspace: "Strg + Alt + ←",
|
||||||
|
|
||||||
|
// German action keys
|
||||||
|
AltLeft: "Alt",
|
||||||
|
AltRight: "AltGr",
|
||||||
|
Backspace: "Rücktaste",
|
||||||
|
"(Backspace)": "Rücktaste",
|
||||||
|
CapsLock: "Feststelltaste",
|
||||||
|
Clear: "Entf",
|
||||||
|
ControlLeft: "Strg",
|
||||||
|
ControlRight: "Strg",
|
||||||
|
Delete: "Entf",
|
||||||
|
End: "Ende",
|
||||||
|
Enter: "Eingabe",
|
||||||
|
Escape: "Esc",
|
||||||
|
Home: "Pos1",
|
||||||
|
Insert: "Einfg",
|
||||||
|
Menu: "Menü",
|
||||||
|
MetaLeft: "Meta",
|
||||||
|
MetaRight: "Meta",
|
||||||
|
PageDown: "Bild ↓",
|
||||||
|
PageUp: "Bild ↑",
|
||||||
|
ShiftLeft: "Umschalt",
|
||||||
|
ShiftRight: "Umschalt",
|
||||||
|
|
||||||
|
// German umlauts and ß
|
||||||
|
BracketLeft: "ü",
|
||||||
|
"(BracketLeft)": "Ü",
|
||||||
|
Semicolon: "ö",
|
||||||
|
"(Semicolon)": "Ö",
|
||||||
|
Quote: "ä",
|
||||||
|
"(Quote)": "Ä",
|
||||||
|
Minus: "ß",
|
||||||
|
"(Minus)": "?",
|
||||||
|
Equal: "´",
|
||||||
|
"(Equal)": "`",
|
||||||
|
Backslash: "#",
|
||||||
|
"(Backslash)": "'",
|
||||||
|
|
||||||
|
// Shifted Numbers
|
||||||
|
"(Digit2)": "\"",
|
||||||
|
"(Digit3)": "§",
|
||||||
|
"(Digit6)": "&",
|
||||||
|
"(Digit7)": "/",
|
||||||
|
"(Digit8)": "(",
|
||||||
|
"(Digit9)": ")",
|
||||||
|
"(Digit0)": "=",
|
||||||
|
|
||||||
|
// Additional German symbols
|
||||||
|
Backquote: "^",
|
||||||
|
"(Backquote)": "°",
|
||||||
|
Comma: ",",
|
||||||
|
"(Comma)": ";",
|
||||||
|
Period: ".",
|
||||||
|
"(Period)": ":",
|
||||||
|
Slash: "-",
|
||||||
|
"(Slash)": "_",
|
||||||
|
|
||||||
|
// Numpad
|
||||||
|
NumpadDecimal: "Num ,",
|
||||||
|
NumpadEnter: "Num Eingabe",
|
||||||
|
NumpadInsert: "Einfg",
|
||||||
|
NumpadDelete: "Entf",
|
||||||
|
|
||||||
|
// Modals
|
||||||
|
PrintScreen: "Druck",
|
||||||
|
ScrollLock: "Rollen",
|
||||||
|
"(Pause)": "Unterbr",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const modifierDisplayMap: Record<string, string> = {
|
||||||
|
ShiftLeft: "Umschalt (links)",
|
||||||
|
ShiftRight: "Umschalt (rechts)",
|
||||||
|
ControlLeft: "Strg (links)",
|
||||||
|
ControlRight: "Strg (rechts)",
|
||||||
|
AltLeft: "Alt",
|
||||||
|
AltRight: "AltGr",
|
||||||
|
MetaLeft: "Meta (links)",
|
||||||
|
MetaRight: "Meta (rechts)",
|
||||||
|
AltGr: "AltGr",
|
||||||
|
} as Record<string, string>;
|
||||||
|
|
||||||
|
export const virtualKeyboard = {
|
||||||
|
main: {
|
||||||
|
default: [
|
||||||
|
"CtrlAltDelete AltMetaEscape CtrlAltBackspace",
|
||||||
|
"Escape F1 F2 F3 F4 F5 F6 F7 F8 F9 F10 F11 F12",
|
||||||
|
"Backquote Digit1 Digit2 Digit3 Digit4 Digit5 Digit6 Digit7 Digit8 Digit9 Digit0 Minus Equal Backspace",
|
||||||
|
"Tab KeyQ KeyW KeyE KeyR KeyT KeyY KeyU KeyI KeyO KeyP BracketLeft BracketRight",
|
||||||
|
"CapsLock KeyA KeyS KeyD KeyF KeyG KeyH KeyJ KeyK KeyL Semicolon Quote Backslash Enter",
|
||||||
|
"ShiftLeft KeyZ KeyX KeyC KeyV KeyB KeyN KeyM Comma Period Slash ShiftRight",
|
||||||
|
"ControlLeft MetaLeft AltLeft Space AltGr MetaRight Menu ControlRight",
|
||||||
|
],
|
||||||
|
shift: [
|
||||||
|
"CtrlAltDelete AltMetaEscape CtrlAltBackspace",
|
||||||
|
"Escape F1 F2 F3 F4 F5 F6 F7 F8 F9 F10 F11 F12",
|
||||||
|
"(Backquote) (Digit1) (Digit2) (Digit3) (Digit4) (Digit5) (Digit6) (Digit7) (Digit8) (Digit9) (Digit0) (Minus) (Equal) (Backspace)",
|
||||||
|
"Tab (KeyQ) (KeyW) (KeyE) (KeyR) (KeyT) (KeyY) (KeyU) (KeyI) (KeyO) (KeyP) (BracketLeft) (BracketRight) (Backslash)",
|
||||||
|
"CapsLock (KeyA) (KeyS) (KeyD) (KeyF) (KeyG) (KeyH) (KeyJ) (KeyK) (KeyL) (Semicolon) (Quote) Enter",
|
||||||
|
"ShiftLeft (KeyZ) (KeyX) (KeyC) (KeyV) (KeyB) (KeyN) (KeyM) (Comma) (Period) (Slash) ShiftRight",
|
||||||
|
"ControlLeft MetaLeft AltLeft Space AltGr MetaRight Menu ControlRight",
|
||||||
|
]
|
||||||
|
},
|
||||||
|
control: {
|
||||||
|
default: [
|
||||||
|
"PrintScreen ScrollLock Pause",
|
||||||
|
"Insert Home PageUp",
|
||||||
|
"Delete End PageDown"
|
||||||
|
],
|
||||||
|
shift: [
|
||||||
|
"(PrintScreen) ScrollLock (Pause)",
|
||||||
|
"Insert Home PageUp",
|
||||||
|
"Delete End PageDown"
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
arrows: {
|
||||||
|
default: [
|
||||||
|
" ArrowUp ",
|
||||||
|
"ArrowLeft ArrowDown ArrowRight"],
|
||||||
|
},
|
||||||
|
|
||||||
|
numpad: {
|
||||||
|
numlocked: [
|
||||||
|
"NumLock NumpadDivide NumpadMultiply NumpadSubtract",
|
||||||
|
"Numpad7 Numpad8 Numpad9 NumpadAdd",
|
||||||
|
"Numpad4 Numpad5 Numpad6",
|
||||||
|
"Numpad1 Numpad2 Numpad3 NumpadEnter",
|
||||||
|
"Numpad0 NumpadDecimal",
|
||||||
|
],
|
||||||
|
default: [
|
||||||
|
"NumLock NumpadDivide NumpadMultiply NumpadSubtract",
|
||||||
|
"Home ArrowUp PageUp NumpadAdd",
|
||||||
|
"ArrowLeft Clear ArrowRight",
|
||||||
|
"End ArrowDown PageDown NumpadEnter",
|
||||||
|
"NumpadInsert NumpadDelete",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const de_DE: KeyboardLayout = {
|
export const de_DE: KeyboardLayout = {
|
||||||
isoCode: "de-DE",
|
isoCode: isoCode,
|
||||||
name: name,
|
name: name,
|
||||||
chars: chars
|
chars: chars,
|
||||||
|
keyDisplayMap: keyDisplayMap,
|
||||||
|
modifierDisplayMap: modifierDisplayMap,
|
||||||
|
virtualKeyboard: virtualKeyboard
|
||||||
};
|
};
|
|
@ -1,6 +1,9 @@
|
||||||
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
|
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
|
||||||
|
|
||||||
|
import { en_US } from "./en_US" // for fallback of keyDisplayMap, modifierDisplayMap, and virtualKeyboard
|
||||||
|
|
||||||
const name = "English (UK)";
|
const name = "English (UK)";
|
||||||
|
const isoCode = "en-UK";
|
||||||
|
|
||||||
const chars = {
|
const chars = {
|
||||||
A: { key: "KeyA", shift: true },
|
A: { key: "KeyA", shift: true },
|
||||||
|
@ -107,7 +110,11 @@ const chars = {
|
||||||
} as Record<string, KeyCombo>
|
} as Record<string, KeyCombo>
|
||||||
|
|
||||||
export const en_UK: KeyboardLayout = {
|
export const en_UK: KeyboardLayout = {
|
||||||
isoCode: "en-UK",
|
isoCode: isoCode,
|
||||||
name: name,
|
name: name,
|
||||||
chars: chars
|
chars: chars,
|
||||||
|
// TODO need to localize these maps and layouts
|
||||||
|
keyDisplayMap: en_US.keyDisplayMap,
|
||||||
|
modifierDisplayMap: en_US.modifierDisplayMap,
|
||||||
|
virtualKeyboard: en_US.virtualKeyboard
|
||||||
};
|
};
|
|
@ -1,8 +1,18 @@
|
||||||
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
|
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
|
||||||
|
|
||||||
const name = "English (US)";
|
const name = "English (US)";
|
||||||
|
const isoCode = "en-US";
|
||||||
|
|
||||||
const chars = {
|
// dead keys for "international" 101 keyboards TODO
|
||||||
|
/*
|
||||||
|
const keyAcute = { key: "Quote", control: true, menu: true, mark: "´" } // acute accent
|
||||||
|
const keyCedilla = { key: ".", shift: true, alt: true, mark: "¸" } // cedilla accent
|
||||||
|
const keyComma = { key: "BracketRight", shift: true, altRight: true, mark: "," } // comma accent
|
||||||
|
const keyDiaeresis = { key: "Quote", shift: true, control: true, menu: true, mark: "¨" } // diaeresis accent
|
||||||
|
const keyDegree = { key: "Semicolon", shift: true, control: true, menu: true, mark: "°" } // degree accent
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const chars = {
|
||||||
A: { key: "KeyA", shift: true },
|
A: { key: "KeyA", shift: true },
|
||||||
B: { key: "KeyB", shift: true },
|
B: { key: "KeyB", shift: true },
|
||||||
C: { key: "KeyC", shift: true },
|
C: { key: "KeyC", shift: true },
|
||||||
|
@ -89,31 +99,213 @@ const chars = {
|
||||||
">": { key: "Period", shift: true },
|
">": { key: "Period", shift: true },
|
||||||
";": { key: "Semicolon" },
|
";": { key: "Semicolon" },
|
||||||
":": { key: "Semicolon", shift: true },
|
":": { key: "Semicolon", shift: true },
|
||||||
|
"¶": { key: "Semicolon", altRight: true }, // pilcrow sign
|
||||||
"[": { key: "BracketLeft" },
|
"[": { key: "BracketLeft" },
|
||||||
"{": { key: "BracketLeft", shift: true },
|
"{": { key: "BracketLeft", shift: true },
|
||||||
|
"«": { key: "BracketLeft", altRight: true }, // double left quote sign
|
||||||
"]": { key: "BracketRight" },
|
"]": { key: "BracketRight" },
|
||||||
"}": { key: "BracketRight", shift: true },
|
"}": { key: "BracketRight", shift: true },
|
||||||
|
"»": { key: "BracketRight", altRight: true }, // double right quote sign
|
||||||
"\\": { key: "Backslash" },
|
"\\": { key: "Backslash" },
|
||||||
"|": { key: "Backslash", shift: true },
|
"|": { key: "Backslash", shift: true },
|
||||||
|
"¬": { key: "Backslash", altRight: true }, // not sign
|
||||||
"`": { key: "Backquote" },
|
"`": { key: "Backquote" },
|
||||||
"~": { key: "Backquote", shift: true },
|
"~": { key: "Backquote", shift: true },
|
||||||
"§": { key: "IntlBackslash" },
|
"§": { key: "IntlBackslash" },
|
||||||
"±": { key: "IntlBackslash", shift: true },
|
"±": { key: "IntlBackslash", shift: true },
|
||||||
" ": { key: "Space", shift: false },
|
" ": { key: "Space" },
|
||||||
"\n": { key: "Enter", shift: false },
|
"\n": { key: "Enter" },
|
||||||
Enter: { key: "Enter", shift: false },
|
Enter: { key: "Enter" },
|
||||||
Tab: { key: "Tab", shift: false },
|
Escape: { key: "Escape" },
|
||||||
PrintScreen: { key: "Prt Sc", shift: false },
|
Tab: { key: "Tab" },
|
||||||
|
PrintScreen: { key: "Prt Sc" },
|
||||||
SystemRequest: { key: "Prt Sc", shift: true },
|
SystemRequest: { key: "Prt Sc", shift: true },
|
||||||
ScrollLock: { key: "ScrollLock", shift: false},
|
ScrollLock: { key: "ScrollLock" },
|
||||||
Pause: { key: "Pause", shift: false },
|
Pause: { key: "Pause" },
|
||||||
Break: { key: "Pause", shift: true },
|
Break: { key: "Pause", shift: true },
|
||||||
Insert: { key: "Insert", shift: false },
|
Insert: { key: "Insert" },
|
||||||
Delete: { key: "Delete", shift: false },
|
Delete: { key: "Delete" },
|
||||||
} as Record<string, KeyCombo>
|
} as Record<string, KeyCombo>
|
||||||
|
|
||||||
export const en_US: KeyboardLayout = {
|
export const modifierDisplayMap: Record<string, string> = {
|
||||||
isoCode: "en-US",
|
ControlLeft: "Left Ctrl",
|
||||||
name: name,
|
ControlRight: "Right Ctrl",
|
||||||
chars: chars
|
ShiftLeft: "Left Shift",
|
||||||
|
ShiftRight: "Right Shift",
|
||||||
|
AltLeft: "Left Alt",
|
||||||
|
AltRight: "Right Alt",
|
||||||
|
MetaLeft: "Left Meta",
|
||||||
|
MetaRight: "Right Meta",
|
||||||
|
AltGr: "AltGr",
|
||||||
|
} as Record<string, string>;
|
||||||
|
|
||||||
|
export const keyDisplayMap: Record<string, string> = {
|
||||||
|
CtrlAltDelete: "Ctrl + Alt + Delete",
|
||||||
|
AltMetaEscape: "Alt + Meta + Escape",
|
||||||
|
CtrlAltBackspace: "Ctrl + Alt + Backspace",
|
||||||
|
AltGr: "AltGr",
|
||||||
|
AltLeft: "Alt",
|
||||||
|
AltRight: "Alt",
|
||||||
|
ArrowDown: "↓",
|
||||||
|
ArrowLeft: "←",
|
||||||
|
ArrowRight: "→",
|
||||||
|
ArrowUp: "↑",
|
||||||
|
Backspace: "Backspace",
|
||||||
|
"(Backspace)": "Backspace",
|
||||||
|
CapsLock: "Caps Lock",
|
||||||
|
Clear: "Clear",
|
||||||
|
ControlLeft: "Ctrl",
|
||||||
|
ControlRight: "Ctrl",
|
||||||
|
Delete: "Delete",
|
||||||
|
End: "End",
|
||||||
|
Enter: "Enter",
|
||||||
|
Escape: "Esc",
|
||||||
|
Home: "Home",
|
||||||
|
Insert: "Insert",
|
||||||
|
Menu: "Menu",
|
||||||
|
MetaLeft: "Meta",
|
||||||
|
MetaRight: "Meta",
|
||||||
|
PageDown: "PgDn",
|
||||||
|
PageUp: "PgUp",
|
||||||
|
ShiftLeft: "Shift",
|
||||||
|
ShiftRight: "Shift",
|
||||||
|
Space: " ",
|
||||||
|
Tab: "Tab",
|
||||||
|
|
||||||
|
// Letters
|
||||||
|
KeyA: "a", KeyB: "b", KeyC: "c", KeyD: "d", KeyE: "e",
|
||||||
|
KeyF: "f", KeyG: "g", KeyH: "h", KeyI: "i", KeyJ: "j",
|
||||||
|
KeyK: "k", KeyL: "l", KeyM: "m", KeyN: "n", KeyO: "o",
|
||||||
|
KeyP: "p", KeyQ: "q", KeyR: "r", KeyS: "s", KeyT: "t",
|
||||||
|
KeyU: "u", KeyV: "v", KeyW: "w", KeyX: "x", KeyY: "y",
|
||||||
|
KeyZ: "z",
|
||||||
|
|
||||||
|
// Capital letters
|
||||||
|
"(KeyA)": "A", "(KeyB)": "B", "(KeyC)": "C", "(KeyD)": "D", "(KeyE)": "E",
|
||||||
|
"(KeyF)": "F", "(KeyG)": "G", "(KeyH)": "H", "(KeyI)": "I", "(KeyJ)": "J",
|
||||||
|
"(KeyK)": "K", "(KeyL)": "L", "(KeyM)": "M", "(KeyN)": "N", "(KeyO)": "O",
|
||||||
|
"(KeyP)": "P", "(KeyQ)": "Q", "(KeyR)": "R", "(KeyS)": "S", "(KeyT)": "T",
|
||||||
|
"(KeyU)": "U", "(KeyV)": "V", "(KeyW)": "W", "(KeyX)": "X", "(KeyY)": "Y",
|
||||||
|
"(KeyZ)": "Z",
|
||||||
|
|
||||||
|
// Numbers
|
||||||
|
Digit1: "1", Digit2: "2", Digit3: "3", Digit4: "4", Digit5: "5",
|
||||||
|
Digit6: "6", Digit7: "7", Digit8: "8", Digit9: "9", Digit0: "0",
|
||||||
|
|
||||||
|
// Shifted Numbers
|
||||||
|
"(Digit1)": "!", "(Digit2)": "@", "(Digit3)": "#", "(Digit4)": "$", "(Digit5)": "%",
|
||||||
|
"(Digit6)": "^", "(Digit7)": "&", "(Digit8)": "*", "(Digit9)": "(", "(Digit0)": ")",
|
||||||
|
|
||||||
|
// Symbols
|
||||||
|
Minus: "-",
|
||||||
|
"(Minus)": "_",
|
||||||
|
Equal: "=",
|
||||||
|
"(Equal)": "+",
|
||||||
|
BracketLeft: "[",
|
||||||
|
"(BracketLeft)": "{",
|
||||||
|
BracketRight: "]",
|
||||||
|
"(BracketRight)": "}",
|
||||||
|
Backslash: "\\",
|
||||||
|
"(Backslash)": "|",
|
||||||
|
Semicolon: ";",
|
||||||
|
"(Semicolon)": ":",
|
||||||
|
Quote: "'",
|
||||||
|
"(Quote)": "\"",
|
||||||
|
Comma: ",",
|
||||||
|
"(Comma)": "<",
|
||||||
|
Period: ".",
|
||||||
|
"(Period)": ">",
|
||||||
|
Slash: "/",
|
||||||
|
"(Slash)": "?",
|
||||||
|
Backquote: "`",
|
||||||
|
"(Backquote)": "~",
|
||||||
|
IntlBackslash: "\\",
|
||||||
|
|
||||||
|
// Function keys
|
||||||
|
F1: "F1", F2: "F2", F3: "F3", F4: "F4",
|
||||||
|
F5: "F5", F6: "F6", F7: "F7", F8: "F8",
|
||||||
|
F9: "F9", F10: "F10", F11: "F11", F12: "F12",
|
||||||
|
|
||||||
|
// Numpad
|
||||||
|
Numpad0: "Num 0", Numpad1: "Num 1", Numpad2: "Num 2",
|
||||||
|
Numpad3: "Num 3", Numpad4: "Num 4", Numpad5: "Num 5",
|
||||||
|
Numpad6: "Num 6", Numpad7: "Num 7", Numpad8: "Num 8",
|
||||||
|
Numpad9: "Num 9", NumpadAdd: "Num +", NumpadSubtract: "Num -",
|
||||||
|
NumpadMultiply: "Num *", NumpadDivide: "Num /", NumpadDecimal: "Num .",
|
||||||
|
NumpadEqual: "Num =", NumpadEnter: "Num Enter", NumpadInsert: "Ins",
|
||||||
|
NumpadDelete: "Del", NumLock: "Num Lock",
|
||||||
|
|
||||||
|
// Modals
|
||||||
|
PrintScreen: "Prt Sc", ScrollLock: "Scr Lk", Pause: "Pause",
|
||||||
|
"(PrintScreen)": "Sys Rq", "(Pause)": "Break",
|
||||||
|
SystemRequest: "Sys Rq", Break: "Break"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const virtualKeyboard = {
|
||||||
|
main: {
|
||||||
|
default: [
|
||||||
|
"CtrlAltDelete AltMetaEscape CtrlAltBackspace",
|
||||||
|
"Escape F1 F2 F3 F4 F5 F6 F7 F8 F9 F10 F11 F12",
|
||||||
|
"Backquote Digit1 Digit2 Digit3 Digit4 Digit5 Digit6 Digit7 Digit8 Digit9 Digit0 Minus Equal Backspace",
|
||||||
|
"Tab KeyQ KeyW KeyE KeyR KeyT KeyY KeyU KeyI KeyO KeyP BracketLeft BracketRight Backslash",
|
||||||
|
"CapsLock KeyA KeyS KeyD KeyF KeyG KeyH KeyJ KeyK KeyL Semicolon Quote Enter",
|
||||||
|
"ShiftLeft KeyZ KeyX KeyC KeyV KeyB KeyN KeyM Comma Period Slash ShiftRight",
|
||||||
|
"ControlLeft MetaLeft AltLeft Space AltGr MetaRight Menu ControlRight",
|
||||||
|
],
|
||||||
|
shift: [
|
||||||
|
"CtrlAltDelete AltMetaEscape CtrlAltBackspace",
|
||||||
|
"Escape F1 F2 F3 F4 F5 F6 F7 F8 F9 F10 F11 F12",
|
||||||
|
"(Backquote) (Digit1) (Digit2) (Digit3) (Digit4) (Digit5) (Digit6) (Digit7) (Digit8) (Digit9) (Digit0) (Minus) (Equal) (Backspace)",
|
||||||
|
"Tab (KeyQ) (KeyW) (KeyE) (KeyR) (KeyT) (KeyY) (KeyU) (KeyI) (KeyO) (KeyP) (BracketLeft) (BracketRight) (Backslash)",
|
||||||
|
"CapsLock (KeyA) (KeyS) (KeyD) (KeyF) (KeyG) (KeyH) (KeyJ) (KeyK) (KeyL) (Semicolon) (Quote) Enter",
|
||||||
|
"ShiftLeft (KeyZ) (KeyX) (KeyC) (KeyV) (KeyB) (KeyN) (KeyM) (Comma) (Period) (Slash) ShiftRight",
|
||||||
|
"ControlLeft MetaLeft AltLeft Space AltGr MetaRight Menu ControlRight",
|
||||||
|
]
|
||||||
|
},
|
||||||
|
control: {
|
||||||
|
default: [
|
||||||
|
"PrintScreen ScrollLock Pause",
|
||||||
|
"Insert Home PageUp",
|
||||||
|
"Delete End PageDown"
|
||||||
|
],
|
||||||
|
shift: [
|
||||||
|
"(PrintScreen) ScrollLock (Pause)",
|
||||||
|
"Insert Home PageUp",
|
||||||
|
"Delete End PageDown"
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
arrows: {
|
||||||
|
default: [
|
||||||
|
"ArrowUp",
|
||||||
|
"ArrowLeft ArrowDown ArrowRight"],
|
||||||
|
},
|
||||||
|
|
||||||
|
numpad: {
|
||||||
|
numlocked: [
|
||||||
|
"NumLock NumpadDivide NumpadMultiply NumpadSubtract",
|
||||||
|
"Numpad7 Numpad8 Numpad9 NumpadAdd",
|
||||||
|
"Numpad4 Numpad5 Numpad6",
|
||||||
|
"Numpad1 Numpad2 Numpad3 NumpadEnter",
|
||||||
|
"Numpad0 NumpadDecimal",
|
||||||
|
],
|
||||||
|
default: [
|
||||||
|
"NumLock NumpadDivide NumpadMultiply NumpadSubtract",
|
||||||
|
"Home ArrowUp PageUp NumpadAdd",
|
||||||
|
"ArrowLeft Clear ArrowRight",
|
||||||
|
"End ArrowDown PageDown NumpadEnter",
|
||||||
|
"NumpadInsert NumpadDelete",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const en_US: KeyboardLayout = {
|
||||||
|
isoCode,
|
||||||
|
name,
|
||||||
|
chars,
|
||||||
|
keyDisplayMap,
|
||||||
|
modifierDisplayMap,
|
||||||
|
virtualKeyboard
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
|
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
|
||||||
|
|
||||||
const name = "Español";
|
import { en_US } from "./en_US" // for fallback of keyDisplayMap, modifierDisplayMap, and virtualKeyboard
|
||||||
|
|
||||||
const keyTrema = { key: "Quote", shift: true } // tréma (umlaut), two dots placed above a vowel
|
const name = "Español";
|
||||||
const keyAcute = { key: "Quote" } // accent aigu (acute accent), mark ´ placed above the letter
|
const isoCode = "es-ES";
|
||||||
const keyHat = { key: "BracketRight", shift: true } // accent circonflexe (accent hat), mark ^ placed above the letter
|
|
||||||
const keyGrave = { key: "BracketRight" } // accent grave, mark ` placed above the letter
|
const keyTrema: KeyCombo = { key: "Quote", shift: true } // tréma (umlaut), two dots placed above a vowel
|
||||||
const keyTilde = { key: "Key4", altRight: true } // tilde, mark ~ placed above the letter
|
const keyAcute: KeyCombo = { key: "Quote" } // accent aigu (acute accent), mark ´ placed above the letter
|
||||||
|
const keyHat: KeyCombo = { key: "BracketRight", shift: true } // accent circonflexe (accent hat), mark ^ placed above the letter
|
||||||
|
const keyGrave: KeyCombo = { key: "BracketRight" } // accent grave, mark ` placed above the letter
|
||||||
|
const keyTilde: KeyCombo = { key: "Key4", altRight: true } // tilde, mark ~ placed above the letter
|
||||||
|
|
||||||
const chars = {
|
const chars = {
|
||||||
A: { key: "KeyA", shift: true },
|
A: { key: "KeyA", shift: true },
|
||||||
|
@ -168,7 +171,11 @@ const chars = {
|
||||||
} as Record<string, KeyCombo>;
|
} as Record<string, KeyCombo>;
|
||||||
|
|
||||||
export const es_ES: KeyboardLayout = {
|
export const es_ES: KeyboardLayout = {
|
||||||
isoCode: "es-ES",
|
isoCode: isoCode,
|
||||||
name: name,
|
name: name,
|
||||||
chars: chars
|
chars: chars,
|
||||||
|
// TODO need to localize these maps and layouts
|
||||||
|
keyDisplayMap: en_US.keyDisplayMap,
|
||||||
|
modifierDisplayMap: en_US.modifierDisplayMap,
|
||||||
|
virtualKeyboard: en_US.virtualKeyboard
|
||||||
};
|
};
|
|
@ -1,12 +1,15 @@
|
||||||
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
|
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
|
||||||
|
|
||||||
const name = "Belgisch Nederlands";
|
import { en_US } from "./en_US" // for fallback of keyDisplayMap, modifierDisplayMap, and virtualKeyboard
|
||||||
|
|
||||||
const keyTrema = { key: "BracketLeft", shift: true } // tréma (umlaut), two dots placed above a vowel
|
const name = "Belgisch Nederlands";
|
||||||
const keyHat = { key: "BracketLeft" } // accent circonflexe (accent hat), mark ^ placed above the letter
|
const isoCode = "nl-BE";
|
||||||
const keyAcute = { key: "Semicolon", altRight: true } // accent aigu (acute accent), mark ´ placed above the letter
|
|
||||||
const keyGrave = { key: "Quote", shift: true } // accent grave, mark ` placed above the letter
|
const keyTrema: KeyCombo = { key: "BracketLeft", shift: true } // tréma (umlaut), two dots placed above a vowel
|
||||||
const keyTilde = { key: "Slash", altRight: true } // tilde, mark ~ placed above the letter
|
const keyHat: KeyCombo = { key: "BracketLeft" } // accent circonflexe (accent hat), mark ^ placed above the letter
|
||||||
|
const keyAcute: KeyCombo = { key: "Semicolon", altRight: true } // accent aigu (acute accent), mark ´ placed above the letter
|
||||||
|
const keyGrave: KeyCombo = { key: "Quote", shift: true } // accent grave, mark ` placed above the letter
|
||||||
|
const keyTilde: KeyCombo = { key: "Slash", altRight: true } // tilde, mark ~ placed above the letter
|
||||||
|
|
||||||
const chars = {
|
const chars = {
|
||||||
A: { key: "KeyQ", shift: true },
|
A: { key: "KeyQ", shift: true },
|
||||||
|
@ -167,7 +170,11 @@ const chars = {
|
||||||
} as Record<string, KeyCombo>;
|
} as Record<string, KeyCombo>;
|
||||||
|
|
||||||
export const fr_BE: KeyboardLayout = {
|
export const fr_BE: KeyboardLayout = {
|
||||||
isoCode: "fr-BE",
|
isoCode: isoCode,
|
||||||
name: name,
|
name: name,
|
||||||
chars: chars
|
chars: chars,
|
||||||
|
// TODO need to localize these maps and layouts
|
||||||
|
keyDisplayMap: en_US.keyDisplayMap,
|
||||||
|
modifierDisplayMap: en_US.modifierDisplayMap,
|
||||||
|
virtualKeyboard: en_US.virtualKeyboard
|
||||||
};
|
};
|
|
@ -3,6 +3,7 @@ import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
|
||||||
import { de_CH } from "./de_CH"
|
import { de_CH } from "./de_CH"
|
||||||
|
|
||||||
const name = "Français de Suisse";
|
const name = "Français de Suisse";
|
||||||
|
const isoCode = "fr-CH";
|
||||||
|
|
||||||
const chars = {
|
const chars = {
|
||||||
...de_CH.chars,
|
...de_CH.chars,
|
||||||
|
@ -14,8 +15,22 @@ const chars = {
|
||||||
"ä": { key: "Quote", shift: true },
|
"ä": { key: "Quote", shift: true },
|
||||||
} as Record<string, KeyCombo>;
|
} as Record<string, KeyCombo>;
|
||||||
|
|
||||||
|
const keyDisplayMap = {
|
||||||
|
...de_CH.keyDisplayMap,
|
||||||
|
"BracketLeft": "è",
|
||||||
|
"BracketLeftShift": "ü",
|
||||||
|
"Semicolon": "é",
|
||||||
|
"SemicolonShift": "ö",
|
||||||
|
"Quote": "à",
|
||||||
|
"QuoteShift": "ä",
|
||||||
|
} as Record<string, string>;
|
||||||
|
|
||||||
export const fr_CH: KeyboardLayout = {
|
export const fr_CH: KeyboardLayout = {
|
||||||
isoCode: "fr-CH",
|
isoCode: isoCode,
|
||||||
name: name,
|
name: name,
|
||||||
chars: chars
|
chars: chars,
|
||||||
|
keyDisplayMap: keyDisplayMap,
|
||||||
|
// TODO need to localize these maps and layouts
|
||||||
|
modifierDisplayMap: de_CH.modifierDisplayMap,
|
||||||
|
virtualKeyboard: de_CH.virtualKeyboard
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
|
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
|
||||||
|
|
||||||
const name = "Français";
|
import { en_US } from "./en_US" // for fallback of keyDisplayMap, modifierDisplayMap, and virtualKeyboard
|
||||||
|
|
||||||
const keyTrema = { key: "BracketLeft", shift: true } // tréma (umlaut), two dots placed above a vowel
|
const name = "Français";
|
||||||
const keyHat = { key: "BracketLeft" } // accent circonflexe (accent hat), mark ^ placed above the letter
|
const isoCode = "fr-FR";
|
||||||
|
|
||||||
|
const keyTrema: KeyCombo = { key: "BracketLeft", shift: true } // tréma (umlaut), two dots placed above a vowel
|
||||||
|
const keyHat: KeyCombo = { key: "BracketLeft" } // accent circonflexe (accent hat), mark ^ placed above the letter
|
||||||
|
|
||||||
const chars = {
|
const chars = {
|
||||||
A: { key: "KeyQ", shift: true },
|
A: { key: "KeyQ", shift: true },
|
||||||
|
@ -139,7 +142,11 @@ const chars = {
|
||||||
} as Record<string, KeyCombo>;
|
} as Record<string, KeyCombo>;
|
||||||
|
|
||||||
export const fr_FR: KeyboardLayout = {
|
export const fr_FR: KeyboardLayout = {
|
||||||
isoCode: "fr-FR",
|
isoCode: isoCode,
|
||||||
name: name,
|
name: name,
|
||||||
chars: chars
|
chars: chars,
|
||||||
|
// TODO need to localize these maps and layouts
|
||||||
|
keyDisplayMap: en_US.keyDisplayMap,
|
||||||
|
modifierDisplayMap: en_US.modifierDisplayMap,
|
||||||
|
virtualKeyboard: en_US.virtualKeyboard
|
||||||
};
|
};
|
|
@ -1,6 +1,9 @@
|
||||||
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
|
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
|
||||||
|
|
||||||
|
import { en_US } from "./en_US" // for fallback of keyDisplayMap, modifierDisplayMap, and virtualKeyboard
|
||||||
|
|
||||||
const name = "Italiano";
|
const name = "Italiano";
|
||||||
|
const isoCode = "it-IT";
|
||||||
|
|
||||||
const chars = {
|
const chars = {
|
||||||
A: { key: "KeyA", shift: true },
|
A: { key: "KeyA", shift: true },
|
||||||
|
@ -113,7 +116,11 @@ const chars = {
|
||||||
} as Record<string, KeyCombo>;
|
} as Record<string, KeyCombo>;
|
||||||
|
|
||||||
export const it_IT: KeyboardLayout = {
|
export const it_IT: KeyboardLayout = {
|
||||||
isoCode: "it-IT",
|
isoCode: isoCode,
|
||||||
name: name,
|
name: name,
|
||||||
chars: chars
|
chars: chars,
|
||||||
|
// TODO need to localize these maps and layouts
|
||||||
|
keyDisplayMap: en_US.keyDisplayMap,
|
||||||
|
modifierDisplayMap: en_US.modifierDisplayMap,
|
||||||
|
virtualKeyboard: en_US.virtualKeyboard
|
||||||
};
|
};
|
|
@ -1,12 +1,15 @@
|
||||||
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
|
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
|
||||||
|
|
||||||
const name = "Norsk bokmål";
|
import { en_US } from "./en_US" // for fallback of keyDisplayMap, modifierDisplayMap, and virtualKeyboard
|
||||||
|
|
||||||
const keyTrema = { key: "BracketRight" } // tréma (umlaut), two dots placed above a vowel
|
const name = "Norsk bokmål";
|
||||||
const keyAcute = { key: "Equal", altRight: true } // accent aigu (acute accent), mark ´ placed above the letter
|
const isoCode = "nb-NO";
|
||||||
const keyHat = { key: "BracketRight", shift: true } // accent circonflexe (accent hat), mark ^ placed above the letter
|
|
||||||
const keyGrave = { key: "Equal", shift: true } // accent grave, mark ` placed above the letter
|
const keyTrema: KeyCombo = { key: "BracketRight" } // tréma (umlaut), two dots placed above a vowel
|
||||||
const keyTilde = { key: "BracketRight", altRight: true } // tilde, mark ~ placed above the letter
|
const keyAcute: KeyCombo = { key: "Equal", altRight: true } // accent aigu (acute accent), mark ´ placed above the letter
|
||||||
|
const keyHat: KeyCombo = { key: "BracketRight", shift: true } // accent circonflexe (accent hat), mark ^ placed above the letter
|
||||||
|
const keyGrave: KeyCombo = { key: "Equal", shift: true } // accent grave, mark ` placed above the letter
|
||||||
|
const keyTilde: KeyCombo = { key: "BracketRight", altRight: true } // tilde, mark ~ placed above the letter
|
||||||
|
|
||||||
const chars = {
|
const chars = {
|
||||||
A: { key: "KeyA", shift: true },
|
A: { key: "KeyA", shift: true },
|
||||||
|
@ -167,7 +170,11 @@ const chars = {
|
||||||
} as Record<string, KeyCombo>;
|
} as Record<string, KeyCombo>;
|
||||||
|
|
||||||
export const nb_NO: KeyboardLayout = {
|
export const nb_NO: KeyboardLayout = {
|
||||||
isoCode: "nb-NO",
|
isoCode: isoCode,
|
||||||
name: name,
|
name: name,
|
||||||
chars: chars
|
chars: chars,
|
||||||
|
// TODO need to localize these maps and layouts
|
||||||
|
keyDisplayMap: en_US.keyDisplayMap,
|
||||||
|
modifierDisplayMap: en_US.modifierDisplayMap,
|
||||||
|
virtualKeyboard: en_US.virtualKeyboard
|
||||||
};
|
};
|
|
@ -1,12 +1,15 @@
|
||||||
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
|
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
|
||||||
|
|
||||||
const name = "Svenska";
|
import { en_US } from "./en_US" // for fallback of keyDisplayMap, modifierDisplayMap, and virtualKeyboard
|
||||||
|
|
||||||
const keyTrema = { key: "BracketRight" } // tréma (umlaut), two dots placed above a vowel
|
const name = "Svenska";
|
||||||
const keyAcute = { key: "Equal" } // accent aigu (acute accent), mark ´ placed above the letter
|
const isoCode = "sv-SE";
|
||||||
const keyHat = { key: "BracketRight", shift: true } // accent circonflexe (accent hat), mark ^ placed above the letter
|
|
||||||
const keyGrave = { key: "Equal", shift: true } // accent grave, mark ` placed above the letter
|
const keyTrema: KeyCombo = { key: "BracketRight" } // tréma (umlaut), two dots placed above a vowel
|
||||||
const keyTilde = { key: "BracketRight", altRight: true } // tilde, mark ~ placed above the letter
|
const keyAcute: KeyCombo = { key: "Equal" } // accent aigu (acute accent), mark ´ placed above the letter
|
||||||
|
const keyHat: KeyCombo = { key: "BracketRight", shift: true } // accent circonflexe (accent hat), mark ^ placed above the letter
|
||||||
|
const keyGrave: KeyCombo = { key: "Equal", shift: true } // accent grave, mark ` placed above the letter
|
||||||
|
const keyTilde: KeyCombo = { key: "BracketRight", altRight: true } // tilde, mark ~ placed above the letter
|
||||||
|
|
||||||
const chars = {
|
const chars = {
|
||||||
A: { key: "KeyA", shift: true },
|
A: { key: "KeyA", shift: true },
|
||||||
|
@ -164,7 +167,11 @@ const chars = {
|
||||||
} as Record<string, KeyCombo>;
|
} as Record<string, KeyCombo>;
|
||||||
|
|
||||||
export const sv_SE: KeyboardLayout = {
|
export const sv_SE: KeyboardLayout = {
|
||||||
isoCode: "sv-SE",
|
isoCode: isoCode,
|
||||||
name: name,
|
name: name,
|
||||||
chars: chars
|
chars: chars,
|
||||||
|
// TODO need to localize these maps and layouts
|
||||||
|
keyDisplayMap: en_US.keyDisplayMap,
|
||||||
|
modifierDisplayMap: en_US.modifierDisplayMap,
|
||||||
|
virtualKeyboard: en_US.virtualKeyboard
|
||||||
};
|
};
|
|
@ -1,20 +1,39 @@
|
||||||
// Key codes and modifiers correspond to definitions in the
|
// Key codes and modifiers correspond to definitions in the
|
||||||
// [Linux USB HID gadget driver](https://www.kernel.org/doc/Documentation/usb/gadget_hid.txt)
|
// [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)
|
// [Universal Serial Bus HID Usage Tables: Section 10](https://www.usb.org/sites/default/files/documents/hut1_12v2.pdf)
|
||||||
|
// These are all the key codes (not scan codes) that an 85/101/102 keyboard might have on it
|
||||||
export const keys = {
|
export const keys = {
|
||||||
|
Again: 0x79,
|
||||||
|
AlternateErase: 0x9d,
|
||||||
|
AltGr: 0xe6, // aka AltRight
|
||||||
|
AltLeft: 0xe2,
|
||||||
|
AltRight: 0xe6,
|
||||||
|
Application: 0x65,
|
||||||
ArrowDown: 0x51,
|
ArrowDown: 0x51,
|
||||||
ArrowLeft: 0x50,
|
ArrowLeft: 0x50,
|
||||||
ArrowRight: 0x4f,
|
ArrowRight: 0x4f,
|
||||||
ArrowUp: 0x52,
|
ArrowUp: 0x52,
|
||||||
|
Attention: 0x9a,
|
||||||
Backquote: 0x35, // aka Grave
|
Backquote: 0x35, // aka Grave
|
||||||
Backslash: 0x31,
|
Backslash: 0x31,
|
||||||
Backspace: 0x2a,
|
Backspace: 0x2a,
|
||||||
BracketLeft: 0x2f, // aka LeftBrace
|
BracketLeft: 0x2f, // aka LeftBrace
|
||||||
BracketRight: 0x30, // aka RightBrace
|
BracketRight: 0x30, // aka RightBrace
|
||||||
|
Cancel: 0x9b,
|
||||||
CapsLock: 0x39,
|
CapsLock: 0x39,
|
||||||
|
Clear: 0x9c,
|
||||||
|
ClearAgain: 0xa2,
|
||||||
Comma: 0x36,
|
Comma: 0x36,
|
||||||
Compose: 0x65,
|
Compose: 0xe3,
|
||||||
ContextMenu: 0,
|
ContextMenu: 0x65,
|
||||||
|
ControlLeft: 0xe0,
|
||||||
|
ControlRight: 0xe4,
|
||||||
|
Copy: 0x7c,
|
||||||
|
CrSel: 0xa3,
|
||||||
|
CurrencySubunit: 0xb5,
|
||||||
|
CurrencyUnit: 0xb4,
|
||||||
|
Cut: 0x7b,
|
||||||
|
DecimalSeparator: 0xb3,
|
||||||
Delete: 0x4c,
|
Delete: 0x4c,
|
||||||
Digit0: 0x27,
|
Digit0: 0x27,
|
||||||
Digit1: 0x1e,
|
Digit1: 0x1e,
|
||||||
|
@ -30,6 +49,8 @@ export const keys = {
|
||||||
Enter: 0x28,
|
Enter: 0x28,
|
||||||
Equal: 0x2e,
|
Equal: 0x2e,
|
||||||
Escape: 0x29,
|
Escape: 0x29,
|
||||||
|
Execute: 0x74,
|
||||||
|
ExSel: 0xa4,
|
||||||
F1: 0x3a,
|
F1: 0x3a,
|
||||||
F2: 0x3b,
|
F2: 0x3b,
|
||||||
F3: 0x3c,
|
F3: 0x3c,
|
||||||
|
@ -42,6 +63,7 @@ export const keys = {
|
||||||
F10: 0x43,
|
F10: 0x43,
|
||||||
F11: 0x44,
|
F11: 0x44,
|
||||||
F12: 0x45,
|
F12: 0x45,
|
||||||
|
F13: 0x68,
|
||||||
F14: 0x69,
|
F14: 0x69,
|
||||||
F15: 0x6a,
|
F15: 0x6a,
|
||||||
F16: 0x6b,
|
F16: 0x6b,
|
||||||
|
@ -53,9 +75,21 @@ export const keys = {
|
||||||
F22: 0x71,
|
F22: 0x71,
|
||||||
F23: 0x72,
|
F23: 0x72,
|
||||||
F24: 0x73,
|
F24: 0x73,
|
||||||
Home: 0x4a,
|
Find: 0x7e,
|
||||||
|
Grave: 0x35,
|
||||||
HashTilde: 0x32, // non-US # and ~
|
HashTilde: 0x32, // non-US # and ~
|
||||||
|
Help: 0x75,
|
||||||
|
Home: 0x4a,
|
||||||
Insert: 0x49,
|
Insert: 0x49,
|
||||||
|
International1: 0x87,
|
||||||
|
International2: 0x88,
|
||||||
|
International3: 0x89,
|
||||||
|
International4: 0x8a,
|
||||||
|
International5: 0x8b,
|
||||||
|
International6: 0x8c,
|
||||||
|
International7: 0x8d,
|
||||||
|
International8: 0x8e,
|
||||||
|
International9: 0x8f,
|
||||||
IntlBackslash: 0x64, // non-US \ and |
|
IntlBackslash: 0x64, // non-US \ and |
|
||||||
KeyA: 0x04,
|
KeyA: 0x04,
|
||||||
KeyB: 0x05,
|
KeyB: 0x05,
|
||||||
|
@ -83,11 +117,27 @@ export const keys = {
|
||||||
KeyX: 0x1b,
|
KeyX: 0x1b,
|
||||||
KeyY: 0x1c,
|
KeyY: 0x1c,
|
||||||
KeyZ: 0x1d,
|
KeyZ: 0x1d,
|
||||||
KeypadExclamation: 0xcf,
|
LockingCapsLock: 0x82,
|
||||||
|
LockingNumLock: 0x83,
|
||||||
|
LockingScrollLock: 0x84,
|
||||||
|
Lang1: 0x90, // Hangul/English toggle on Korean keyboards
|
||||||
|
Lang2: 0x91, // Hanja conversion on Korean keyboards
|
||||||
|
Lang3: 0x92, // Katakana on Japanese keyboards
|
||||||
|
Lang4: 0x93, // Hiragana on Japanese keyboards
|
||||||
|
Lang5: 0x94, // Zenkaku/Hankaku toggle on Japanese keyboards
|
||||||
|
Lang6: 0x95,
|
||||||
|
Lang7: 0x96,
|
||||||
|
Lang8: 0x97,
|
||||||
|
Lang9: 0x98,
|
||||||
|
Menu: 0x76,
|
||||||
|
MetaLeft: 0xe3,
|
||||||
|
MetaRight: 0xe7,
|
||||||
Minus: 0x2d,
|
Minus: 0x2d,
|
||||||
None: 0x00,
|
Mute: 0x7f,
|
||||||
NumLock: 0x53, // and Clear
|
NumLock: 0x53, // and Clear
|
||||||
Numpad0: 0x62, // and Insert
|
Numpad0: 0x62, // and Insert
|
||||||
|
Numpad00: 0xb0,
|
||||||
|
Numpad000: 0xb1,
|
||||||
Numpad1: 0x59, // and End
|
Numpad1: 0x59, // and End
|
||||||
Numpad2: 0x5a, // and Down Arrow
|
Numpad2: 0x5a, // and Down Arrow
|
||||||
Numpad3: 0x5b, // and Page Down
|
Numpad3: 0x5b, // and Page Down
|
||||||
|
@ -98,30 +148,111 @@ export const keys = {
|
||||||
Numpad8: 0x60, // and Up Arrow
|
Numpad8: 0x60, // and Up Arrow
|
||||||
Numpad9: 0x61, // and Page Up
|
Numpad9: 0x61, // and Page Up
|
||||||
NumpadAdd: 0x57,
|
NumpadAdd: 0x57,
|
||||||
|
NumpadAnd: 0xc7,
|
||||||
|
NumpadAt: 0xce,
|
||||||
|
NumpadBackspace: 0xbb,
|
||||||
|
NumpadBinary: 0xda,
|
||||||
|
NumpadCircumflex: 0xc3,
|
||||||
|
NumpadClear: 0xd8,
|
||||||
|
NumpadClearEntry: 0xd9,
|
||||||
|
NumpadColon: 0xcb,
|
||||||
NumpadComma: 0x85,
|
NumpadComma: 0x85,
|
||||||
NumpadDecimal: 0x63,
|
NumpadDecimal: 0x63,
|
||||||
|
NumpadDecimalBase: 0xdc,
|
||||||
|
NumpadDelete: 0x63,
|
||||||
NumpadDivide: 0x54,
|
NumpadDivide: 0x54,
|
||||||
|
NumpadDownArrow: 0x5a,
|
||||||
|
NumpadEnd: 0x59,
|
||||||
NumpadEnter: 0x58,
|
NumpadEnter: 0x58,
|
||||||
NumpadEqual: 0x67,
|
NumpadEqual: 0x67,
|
||||||
|
NumpadExclamation: 0xcf,
|
||||||
|
NumpadGreaterThan: 0xc6,
|
||||||
|
NumpadHexadecimal: 0xdd,
|
||||||
|
NumpadHome: 0x5f,
|
||||||
|
NumpadKeyA: 0xbc,
|
||||||
|
NumpadKeyB: 0xbd,
|
||||||
|
NumpadKeyC: 0xbe,
|
||||||
|
NumpadKeyD: 0xbf,
|
||||||
|
NumpadKeyE: 0xc0,
|
||||||
|
NumpadKeyF: 0xc1,
|
||||||
|
NumpadLeftArrow: 0x5c,
|
||||||
|
NumpadLeftBrace: 0xb8,
|
||||||
NumpadLeftParen: 0xb6,
|
NumpadLeftParen: 0xb6,
|
||||||
|
NumpadLessThan: 0xc5,
|
||||||
|
NumpadLogicalAnd: 0xc8,
|
||||||
|
NumpadLogicalOr: 0xca,
|
||||||
|
NumpadMemoryAdd: 0xd3,
|
||||||
|
NumpadMemoryClear: 0xd2,
|
||||||
|
NumpadMemoryDivide: 0xd6,
|
||||||
|
NumpadMemoryMultiply: 0xd5,
|
||||||
|
NumpadMemoryRecall: 0xd1,
|
||||||
|
NumpadMemoryStore: 0xd0,
|
||||||
|
NumpadMemorySubtract: 0xd4,
|
||||||
NumpadMultiply: 0x55,
|
NumpadMultiply: 0x55,
|
||||||
|
NumpadOctal: 0xdb,
|
||||||
|
NumpadOctathorpe: 0xcc,
|
||||||
|
NumpadOr: 0xc9,
|
||||||
|
NumpadPageDown: 0x5b,
|
||||||
|
NumpadPageUp: 0x61,
|
||||||
|
NumpadPercent: 0xc4,
|
||||||
|
NumpadPlusMinus: 0xd7,
|
||||||
|
NumpadRightArrow: 0x5e,
|
||||||
|
NumpadRightBrace: 0xb9,
|
||||||
NumpadRightParen: 0xb7,
|
NumpadRightParen: 0xb7,
|
||||||
|
NumpadSpace: 0xcd,
|
||||||
NumpadSubtract: 0x56,
|
NumpadSubtract: 0x56,
|
||||||
|
NumpadTab: 0xba,
|
||||||
|
NumpadUpArrow: 0x60,
|
||||||
|
NumpadXOR: 0xc2,
|
||||||
|
Octothorpe: 0x32, // non-US # and ~
|
||||||
|
Operation: 0xa1,
|
||||||
|
Out: 0xa0,
|
||||||
PageDown: 0x4e,
|
PageDown: 0x4e,
|
||||||
PageUp: 0x4b,
|
PageUp: 0x4b,
|
||||||
Period: 0x37,
|
Paste: 0x7d,
|
||||||
PrintScreen: 0x46,
|
|
||||||
Pause: 0x48,
|
Pause: 0x48,
|
||||||
|
Period: 0x37,
|
||||||
Power: 0x66,
|
Power: 0x66,
|
||||||
|
PrintScreen: 0x46,
|
||||||
|
Prior: 0x9d,
|
||||||
Quote: 0x34, // aka Single Quote or Apostrophe
|
Quote: 0x34, // aka Single Quote or Apostrophe
|
||||||
|
Return: 0x9e,
|
||||||
ScrollLock: 0x47,
|
ScrollLock: 0x47,
|
||||||
|
Select: 0x77,
|
||||||
Semicolon: 0x33,
|
Semicolon: 0x33,
|
||||||
|
Separator: 0x9f,
|
||||||
|
ShiftLeft: 0xe1,
|
||||||
|
ShiftRight: 0xe5,
|
||||||
Slash: 0x38,
|
Slash: 0x38,
|
||||||
Space: 0x2c,
|
Space: 0x2c,
|
||||||
|
Stop: 0x78,
|
||||||
SystemRequest: 0x9a,
|
SystemRequest: 0x9a,
|
||||||
Tab: 0x2b,
|
Tab: 0x2b,
|
||||||
|
ThousandsSeparator: 0xb2,
|
||||||
|
Tilde: 0x35,
|
||||||
|
Undo: 0x7a,
|
||||||
|
VolumeDown: 0x81,
|
||||||
|
VolumeUp: 0x80,
|
||||||
} as Record<string, number>;
|
} as Record<string, number>;
|
||||||
|
|
||||||
|
export const deadKeys = {
|
||||||
|
Acute: 0x00b4,
|
||||||
|
Breve: 0x02d8,
|
||||||
|
Caron: 0x02c7,
|
||||||
|
Cedilla: 0x00b8,
|
||||||
|
Circumflex: 0x005e, // or 0x02c6?
|
||||||
|
Comma: 0x002c,
|
||||||
|
Dot: 0x00b7,
|
||||||
|
DoubleAcute: 0x02dd,
|
||||||
|
Grave: 0x0060,
|
||||||
|
Kreis: 0x00b0,
|
||||||
|
Ogonek: 0x02db,
|
||||||
|
Ring: 0x02da,
|
||||||
|
Slash: 0x02f8,
|
||||||
|
Tilde: 0x007e,
|
||||||
|
Umlaut: 0x00a8,
|
||||||
|
} as Record<string, number>
|
||||||
|
|
||||||
export const modifiers = {
|
export const modifiers = {
|
||||||
ControlLeft: 0x01,
|
ControlLeft: 0x01,
|
||||||
ControlRight: 0x10,
|
ControlRight: 0x10,
|
||||||
|
@ -131,113 +262,28 @@ export const modifiers = {
|
||||||
AltRight: 0x40,
|
AltRight: 0x40,
|
||||||
MetaLeft: 0x08,
|
MetaLeft: 0x08,
|
||||||
MetaRight: 0x80,
|
MetaRight: 0x80,
|
||||||
|
AltGr: 0x40,
|
||||||
} as Record<string, number>;
|
} as Record<string, number>;
|
||||||
|
|
||||||
export const modifierDisplayMap: Record<string, string> = {
|
export const hidKeyToModifierMask = {
|
||||||
ControlLeft: "Left Ctrl",
|
0xe0: modifiers.ControlLeft,
|
||||||
ControlRight: "Right Ctrl",
|
0xe1: modifiers.ShiftLeft,
|
||||||
ShiftLeft: "Left Shift",
|
0xe2: modifiers.AltLeft,
|
||||||
ShiftRight: "Right Shift",
|
0xe3: modifiers.MetaLeft,
|
||||||
AltLeft: "Left Alt",
|
0xe4: modifiers.ControlRight,
|
||||||
AltRight: "Right Alt",
|
0xe5: modifiers.ShiftRight,
|
||||||
MetaLeft: "Left Meta",
|
0xe6: modifiers.AltRight, // can also be AltGr
|
||||||
MetaRight: "Right Meta",
|
0xe7: modifiers.MetaRight,
|
||||||
} as Record<string, string>;
|
} as Record<number, number>;
|
||||||
|
|
||||||
export const keyDisplayMap: Record<string, string> = {
|
export const latchingKeys = ["CapsLock", "ScrollLock", "NumLock", "Meta", "Compose", "Kana"];
|
||||||
CtrlAltDelete: "Ctrl + Alt + Delete",
|
|
||||||
AltMetaEscape: "Alt + Meta + Escape",
|
|
||||||
CtrlAltBackspace: "Ctrl + Alt + Backspace",
|
|
||||||
Escape: "esc",
|
|
||||||
Tab: "tab",
|
|
||||||
Backspace: "backspace",
|
|
||||||
"(Backspace)": "backspace",
|
|
||||||
Enter: "enter",
|
|
||||||
CapsLock: "caps lock",
|
|
||||||
ShiftLeft: "shift",
|
|
||||||
ShiftRight: "shift",
|
|
||||||
ControlLeft: "ctrl",
|
|
||||||
AltLeft: "alt",
|
|
||||||
AltRight: "alt",
|
|
||||||
MetaLeft: "meta",
|
|
||||||
MetaRight: "meta",
|
|
||||||
Space: " ",
|
|
||||||
Insert: "insert",
|
|
||||||
Home: "home",
|
|
||||||
PageUp: "page up",
|
|
||||||
Delete: "delete",
|
|
||||||
End: "end",
|
|
||||||
PageDown: "page down",
|
|
||||||
ArrowLeft: "←",
|
|
||||||
ArrowRight: "→",
|
|
||||||
ArrowUp: "↑",
|
|
||||||
ArrowDown: "↓",
|
|
||||||
|
|
||||||
// Letters
|
export function decodeModifiers(modifier: number) {
|
||||||
KeyA: "a", KeyB: "b", KeyC: "c", KeyD: "d", KeyE: "e",
|
return {
|
||||||
KeyF: "f", KeyG: "g", KeyH: "h", KeyI: "i", KeyJ: "j",
|
isShiftActive: (modifier & (modifiers.ShiftLeft | modifiers.ShiftRight)) !== 0,
|
||||||
KeyK: "k", KeyL: "l", KeyM: "m", KeyN: "n", KeyO: "o",
|
isControlActive: (modifier & (modifiers.ControlLeft | modifiers.ControlRight)) !== 0,
|
||||||
KeyP: "p", KeyQ: "q", KeyR: "r", KeyS: "s", KeyT: "t",
|
isAltActive: (modifier & (modifiers.AltLeft | modifiers.AltRight)) !== 0,
|
||||||
KeyU: "u", KeyV: "v", KeyW: "w", KeyX: "x", KeyY: "y",
|
isMetaActive: (modifier & (modifiers.MetaLeft | modifiers.MetaRight)) !== 0,
|
||||||
KeyZ: "z",
|
isAltGrActive: (modifier & modifiers.AltGr) !== 0,
|
||||||
|
};
|
||||||
// Capital letters
|
}
|
||||||
"(KeyA)": "A", "(KeyB)": "B", "(KeyC)": "C", "(KeyD)": "D", "(KeyE)": "E",
|
|
||||||
"(KeyF)": "F", "(KeyG)": "G", "(KeyH)": "H", "(KeyI)": "I", "(KeyJ)": "J",
|
|
||||||
"(KeyK)": "K", "(KeyL)": "L", "(KeyM)": "M", "(KeyN)": "N", "(KeyO)": "O",
|
|
||||||
"(KeyP)": "P", "(KeyQ)": "Q", "(KeyR)": "R", "(KeyS)": "S", "(KeyT)": "T",
|
|
||||||
"(KeyU)": "U", "(KeyV)": "V", "(KeyW)": "W", "(KeyX)": "X", "(KeyY)": "Y",
|
|
||||||
"(KeyZ)": "Z",
|
|
||||||
|
|
||||||
// Numbers
|
|
||||||
Digit1: "1", Digit2: "2", Digit3: "3", Digit4: "4", Digit5: "5",
|
|
||||||
Digit6: "6", Digit7: "7", Digit8: "8", Digit9: "9", Digit0: "0",
|
|
||||||
|
|
||||||
// Shifted Numbers
|
|
||||||
"(Digit1)": "!", "(Digit2)": "@", "(Digit3)": "#", "(Digit4)": "$", "(Digit5)": "%",
|
|
||||||
"(Digit6)": "^", "(Digit7)": "&", "(Digit8)": "*", "(Digit9)": "(", "(Digit0)": ")",
|
|
||||||
|
|
||||||
// Symbols
|
|
||||||
Minus: "-",
|
|
||||||
"(Minus)": "_",
|
|
||||||
Equal: "=",
|
|
||||||
"(Equal)": "+",
|
|
||||||
BracketLeft: "[",
|
|
||||||
"(BracketLeft)": "{",
|
|
||||||
BracketRight: "]",
|
|
||||||
"(BracketRight)": "}",
|
|
||||||
Backslash: "\\",
|
|
||||||
"(Backslash)": "|",
|
|
||||||
Semicolon: ";",
|
|
||||||
"(Semicolon)": ":",
|
|
||||||
Quote: "'",
|
|
||||||
"(Quote)": "\"",
|
|
||||||
Comma: ",",
|
|
||||||
"(Comma)": "<",
|
|
||||||
Period: ".",
|
|
||||||
"(Period)": ">",
|
|
||||||
Slash: "/",
|
|
||||||
"(Slash)": "?",
|
|
||||||
Backquote: "`",
|
|
||||||
"(Backquote)": "~",
|
|
||||||
IntlBackslash: "\\",
|
|
||||||
|
|
||||||
// Function keys
|
|
||||||
F1: "F1", F2: "F2", F3: "F3", F4: "F4",
|
|
||||||
F5: "F5", F6: "F6", F7: "F7", F8: "F8",
|
|
||||||
F9: "F9", F10: "F10", F11: "F11", F12: "F12",
|
|
||||||
|
|
||||||
// Numpad
|
|
||||||
Numpad0: "Num 0", Numpad1: "Num 1", Numpad2: "Num 2",
|
|
||||||
Numpad3: "Num 3", Numpad4: "Num 4", Numpad5: "Num 5",
|
|
||||||
Numpad6: "Num 6", Numpad7: "Num 7", Numpad8: "Num 8",
|
|
||||||
Numpad9: "Num 9", NumpadAdd: "Num +", NumpadSubtract: "Num -",
|
|
||||||
NumpadMultiply: "Num *", NumpadDivide: "Num /", NumpadDecimal: "Num .",
|
|
||||||
NumpadEqual: "Num =", NumpadEnter: "Num Enter",
|
|
||||||
NumLock: "Num Lock",
|
|
||||||
|
|
||||||
// Modals
|
|
||||||
PrintScreen: "prt sc", ScrollLock: "scr lk", Pause: "pause",
|
|
||||||
"(PrintScreen)": "sys rq", "(Pause)": "break",
|
|
||||||
SystemRequest: "sys rq", Break: "break"
|
|
||||||
};
|
|
|
@ -64,7 +64,7 @@ export function Dialog({ onClose }: { onClose: () => void }) {
|
||||||
setRemoteVirtualMediaState(null);
|
setRemoteVirtualMediaState(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [send] = useJsonRpc();
|
const { send } = useJsonRpc();
|
||||||
async function syncRemoteVirtualMediaState() {
|
async function syncRemoteVirtualMediaState() {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
send("getVirtualMediaState", {}, resp => {
|
send("getVirtualMediaState", {}, resp => {
|
||||||
|
@ -689,7 +689,7 @@ function DeviceFileView({
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const filesPerPage = 5;
|
const filesPerPage = 5;
|
||||||
|
|
||||||
const [send] = useJsonRpc();
|
const { send } = useJsonRpc();
|
||||||
|
|
||||||
interface StorageSpace {
|
interface StorageSpace {
|
||||||
bytesUsed: number;
|
bytesUsed: number;
|
||||||
|
@ -1001,7 +1001,7 @@ function UploadFileView({
|
||||||
const [fileError, setFileError] = useState<string | null>(null);
|
const [fileError, setFileError] = useState<string | null>(null);
|
||||||
const [uploadError, setUploadError] = useState<string | null>(null);
|
const [uploadError, setUploadError] = useState<string | null>(null);
|
||||||
|
|
||||||
const [send] = useJsonRpc();
|
const { send } = useJsonRpc();
|
||||||
const rtcDataChannelRef = useRef<RTCDataChannel | null>(null);
|
const rtcDataChannelRef = useRef<RTCDataChannel | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
@ -42,7 +42,7 @@ export default function SettingsAccessIndexRoute() {
|
||||||
const { navigateTo } = useDeviceUiNavigation();
|
const { navigateTo } = useDeviceUiNavigation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [send] = useJsonRpc();
|
const { send } = useJsonRpc();
|
||||||
|
|
||||||
const [isAdopted, setAdopted] = useState(false);
|
const [isAdopted, setAdopted] = useState(false);
|
||||||
const [deviceId, setDeviceId] = useState<string | null>(null);
|
const [deviceId, setDeviceId] = useState<string | null>(null);
|
||||||
|
@ -166,9 +166,7 @@ export default function SettingsAccessIndexRoute() {
|
||||||
|
|
||||||
notifications.success("TLS settings updated successfully");
|
notifications.success("TLS settings updated successfully");
|
||||||
});
|
});
|
||||||
},
|
}, [send]);
|
||||||
[send],
|
|
||||||
);
|
|
||||||
|
|
||||||
// Handle TLS mode change
|
// Handle TLS mode change
|
||||||
const handleTlsModeChange = (value: string) => {
|
const handleTlsModeChange = (value: string) => {
|
||||||
|
|
|
@ -15,10 +15,10 @@ import notifications from "../notifications";
|
||||||
import { SettingsItem } from "./devices.$id.settings";
|
import { SettingsItem } from "./devices.$id.settings";
|
||||||
|
|
||||||
export default function SettingsAdvancedRoute() {
|
export default function SettingsAdvancedRoute() {
|
||||||
const [send] = useJsonRpc();
|
const { send } = useJsonRpc();
|
||||||
|
|
||||||
const [sshKey, setSSHKey] = useState<string>("");
|
const [sshKey, setSSHKey] = useState<string>("");
|
||||||
const setDeveloperMode = useSettingsStore(state => state.setDeveloperMode);
|
const { setDeveloperMode } = useSettingsStore();
|
||||||
const [devChannel, setDevChannel] = useState(false);
|
const [devChannel, setDevChannel] = useState(false);
|
||||||
const [usbEmulationEnabled, setUsbEmulationEnabled] = useState(false);
|
const [usbEmulationEnabled, setUsbEmulationEnabled] = useState(false);
|
||||||
const [showLoopbackWarning, setShowLoopbackWarning] = useState(false);
|
const [showLoopbackWarning, setShowLoopbackWarning] = useState(false);
|
||||||
|
|
|
@ -13,7 +13,7 @@ import { useDeviceStore } from "../hooks/stores";
|
||||||
import { SettingsItem } from "./devices.$id.settings";
|
import { SettingsItem } from "./devices.$id.settings";
|
||||||
|
|
||||||
export default function SettingsGeneralRoute() {
|
export default function SettingsGeneralRoute() {
|
||||||
const [send] = useJsonRpc();
|
const { send } = useJsonRpc();
|
||||||
const { navigateTo } = useDeviceUiNavigation();
|
const { navigateTo } = useDeviceUiNavigation();
|
||||||
const [autoUpdate, setAutoUpdate] = useState(true);
|
const [autoUpdate, setAutoUpdate] = useState(true);
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { Button } from "@components/Button";
|
||||||
|
|
||||||
export default function SettingsGeneralRebootRoute() {
|
export default function SettingsGeneralRebootRoute() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [send] = useJsonRpc();
|
const { send } = useJsonRpc();
|
||||||
|
|
||||||
const onConfirmUpdate = useCallback(() => {
|
const onConfirmUpdate = useCallback(() => {
|
||||||
// This is where we send the RPC to the golang binary
|
// This is where we send the RPC to the golang binary
|
||||||
|
|
|
@ -16,7 +16,7 @@ export default function SettingsGeneralUpdateRoute() {
|
||||||
const { updateSuccess } = location.state || {};
|
const { updateSuccess } = location.state || {};
|
||||||
|
|
||||||
const { setModalView, otaState } = useUpdateStore();
|
const { setModalView, otaState } = useUpdateStore();
|
||||||
const [send] = useJsonRpc();
|
const { send } = useJsonRpc();
|
||||||
|
|
||||||
const onConfirmUpdate = useCallback(() => {
|
const onConfirmUpdate = useCallback(() => {
|
||||||
send("tryUpdate", {});
|
send("tryUpdate", {});
|
||||||
|
@ -134,10 +134,8 @@ function LoadingState({
|
||||||
}) {
|
}) {
|
||||||
const [progressWidth, setProgressWidth] = useState("0%");
|
const [progressWidth, setProgressWidth] = useState("0%");
|
||||||
const abortControllerRef = useRef<AbortController | null>(null);
|
const abortControllerRef = useRef<AbortController | null>(null);
|
||||||
const [send] = useJsonRpc();
|
const { send } = useJsonRpc();
|
||||||
|
const { setAppVersion, setSystemVersion } = useDeviceStore();
|
||||||
const setAppVersion = useDeviceStore(state => state.setAppVersion);
|
|
||||||
const setSystemVersion = useDeviceStore(state => state.setSystemVersion);
|
|
||||||
|
|
||||||
const getVersionInfo = useCallback(() => {
|
const getVersionInfo = useCallback(() => {
|
||||||
return new Promise<SystemVersionInfo>((resolve, reject) => {
|
return new Promise<SystemVersionInfo>((resolve, reject) => {
|
||||||
|
|
|
@ -12,10 +12,9 @@ import { UsbInfoSetting } from "../components/UsbInfoSetting";
|
||||||
import { FeatureFlag } from "../components/FeatureFlag";
|
import { FeatureFlag } from "../components/FeatureFlag";
|
||||||
|
|
||||||
export default function SettingsHardwareRoute() {
|
export default function SettingsHardwareRoute() {
|
||||||
const [send] = useJsonRpc();
|
const { send } = useJsonRpc();
|
||||||
const settings = useSettingsStore();
|
const settings = useSettingsStore();
|
||||||
|
const { setDisplayRotation } = useSettingsStore();
|
||||||
const setDisplayRotation = useSettingsStore(state => state.setDisplayRotation);
|
|
||||||
|
|
||||||
const handleDisplayRotationChange = (rotation: string) => {
|
const handleDisplayRotationChange = (rotation: string) => {
|
||||||
setDisplayRotation(rotation);
|
setDisplayRotation(rotation);
|
||||||
|
@ -34,7 +33,7 @@ export default function SettingsHardwareRoute() {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const setBacklightSettings = useSettingsStore(state => state.setBacklightSettings);
|
const { setBacklightSettings } = useSettingsStore();
|
||||||
|
|
||||||
const handleBacklightSettingsChange = (settings: BacklightSettings) => {
|
const handleBacklightSettingsChange = (settings: BacklightSettings) => {
|
||||||
// If the user has set the display to dim after it turns off, set the dim_after
|
// If the user has set the display to dim after it turns off, set the dim_after
|
||||||
|
|
|
@ -1,64 +1,44 @@
|
||||||
import { useCallback, useEffect, useMemo } from "react";
|
import { useCallback, useEffect } from "react";
|
||||||
|
|
||||||
import { KeyboardLedSync, useSettingsStore } from "@/hooks/stores";
|
import { useSettingsStore } from "@/hooks/stores";
|
||||||
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||||
import notifications from "@/notifications";
|
import useKeyboardLayout from "@/hooks/useKeyboardLayout";
|
||||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||||
import { keyboardOptions } from "@/keyboardLayouts";
|
|
||||||
import { Checkbox } from "@/components/Checkbox";
|
import { Checkbox } from "@/components/Checkbox";
|
||||||
|
import { SelectMenuBasic } from "@/components/SelectMenuBasic";
|
||||||
import { SelectMenuBasic } from "../components/SelectMenuBasic";
|
import notifications from "@/notifications";
|
||||||
|
|
||||||
import { SettingsItem } from "./devices.$id.settings";
|
import { SettingsItem } from "./devices.$id.settings";
|
||||||
|
|
||||||
export default function SettingsKeyboardRoute() {
|
export default function SettingsKeyboardRoute() {
|
||||||
const keyboardLayout = useSettingsStore(state => state.keyboardLayout);
|
const { setKeyboardLayout } = useSettingsStore();
|
||||||
const keyboardLedSync = useSettingsStore(state => state.keyboardLedSync);
|
const { showPressedKeys, setShowPressedKeys } = useSettingsStore();
|
||||||
const showPressedKeys = useSettingsStore(state => state.showPressedKeys);
|
const { selectedKeyboard, keyboardOptions } = useKeyboardLayout();
|
||||||
const setKeyboardLayout = useSettingsStore(
|
|
||||||
state => state.setKeyboardLayout,
|
|
||||||
);
|
|
||||||
const setKeyboardLedSync = useSettingsStore(
|
|
||||||
state => state.setKeyboardLedSync,
|
|
||||||
);
|
|
||||||
const setShowPressedKeys = useSettingsStore(
|
|
||||||
state => state.setShowPressedKeys,
|
|
||||||
);
|
|
||||||
|
|
||||||
// this ensures we always get the original en_US if it hasn't been set yet
|
const { send } = useJsonRpc();
|
||||||
const safeKeyboardLayout = useMemo(() => {
|
|
||||||
if (keyboardLayout && keyboardLayout.length > 0)
|
|
||||||
return keyboardLayout;
|
|
||||||
return "en_US";
|
|
||||||
}, [keyboardLayout]);
|
|
||||||
|
|
||||||
const layoutOptions = keyboardOptions();
|
|
||||||
const ledSyncOptions = [
|
|
||||||
{ value: "auto", label: "Automatic" },
|
|
||||||
{ value: "browser", label: "Browser Only" },
|
|
||||||
{ value: "host", label: "Host Only" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const [send] = useJsonRpc();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
send("getKeyboardLayout", {}, resp => {
|
send("getKeyboardLayout", {}, resp => {
|
||||||
if ("error" in resp) return;
|
if ("error" in resp) return;
|
||||||
setKeyboardLayout(resp.result as string);
|
const isoCode = resp.result as string;
|
||||||
|
console.log("Fetched keyboard layout from backend:", isoCode);
|
||||||
|
if (isoCode && isoCode.length > 0) {
|
||||||
|
setKeyboardLayout(isoCode);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}, [send, setKeyboardLayout]);
|
}, [send, setKeyboardLayout]);
|
||||||
|
|
||||||
const onKeyboardLayoutChange = useCallback(
|
const onKeyboardLayoutChange = useCallback(
|
||||||
(e: React.ChangeEvent<HTMLSelectElement>) => {
|
(e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
const layout = e.target.value;
|
const isoCode = e.target.value;
|
||||||
send("setKeyboardLayout", { layout }, resp => {
|
send("setKeyboardLayout", { layout: isoCode }, resp => {
|
||||||
if ("error" in resp) {
|
if ("error" in resp) {
|
||||||
notifications.error(
|
notifications.error(
|
||||||
`Failed to set keyboard layout: ${resp.error.data || "Unknown error"}`,
|
`Failed to set keyboard layout: ${resp.error.data || "Unknown error"}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
notifications.success("Keyboard layout set successfully");
|
notifications.success("Keyboard layout set successfully to " + isoCode);
|
||||||
setKeyboardLayout(layout);
|
setKeyboardLayout(isoCode);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[send, setKeyboardLayout],
|
[send, setKeyboardLayout],
|
||||||
|
@ -72,7 +52,6 @@ export default function SettingsKeyboardRoute() {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{ /* this menu item could be renamed to plain "Keyboard layout" in the future, when also the virtual keyboard layout mappings are being implemented */ }
|
|
||||||
<SettingsItem
|
<SettingsItem
|
||||||
title="Paste text"
|
title="Paste text"
|
||||||
description="Keyboard layout of target operating system"
|
description="Keyboard layout of target operating system"
|
||||||
|
@ -81,9 +60,9 @@ export default function SettingsKeyboardRoute() {
|
||||||
size="SM"
|
size="SM"
|
||||||
label=""
|
label=""
|
||||||
fullWidth
|
fullWidth
|
||||||
value={safeKeyboardLayout}
|
value={selectedKeyboard.isoCode}
|
||||||
onChange={onKeyboardLayoutChange}
|
onChange={onKeyboardLayoutChange}
|
||||||
options={layoutOptions}
|
options={keyboardOptions}
|
||||||
/>
|
/>
|
||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
<p className="text-xs text-slate-600 dark:text-slate-400">
|
<p className="text-xs text-slate-600 dark:text-slate-400">
|
||||||
|
@ -91,23 +70,6 @@ export default function SettingsKeyboardRoute() {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4">
|
|
||||||
{ /* this menu item could be renamed to plain "Keyboard layout" in the future, when also the virtual keyboard layout mappings are being implemented */ }
|
|
||||||
<SettingsItem
|
|
||||||
title="LED state synchronization"
|
|
||||||
description="Synchronize the LED state of the keyboard with the target device"
|
|
||||||
>
|
|
||||||
<SelectMenuBasic
|
|
||||||
size="SM"
|
|
||||||
label=""
|
|
||||||
fullWidth
|
|
||||||
value={keyboardLedSync}
|
|
||||||
onChange={e => setKeyboardLedSync(e.target.value as KeyboardLedSync)}
|
|
||||||
options={ledSyncOptions}
|
|
||||||
/>
|
|
||||||
</SettingsItem>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<SettingsItem
|
<SettingsItem
|
||||||
title="Show Pressed Keys"
|
title="Show Pressed Keys"
|
||||||
|
|
|
@ -17,10 +17,10 @@ import { Button } from "@/components/Button";
|
||||||
import EmptyCard from "@/components/EmptyCard";
|
import EmptyCard from "@/components/EmptyCard";
|
||||||
import Card from "@/components/Card";
|
import Card from "@/components/Card";
|
||||||
import { MAX_TOTAL_MACROS, COPY_SUFFIX, DEFAULT_DELAY } from "@/constants/macros";
|
import { MAX_TOTAL_MACROS, COPY_SUFFIX, DEFAULT_DELAY } from "@/constants/macros";
|
||||||
import { keyDisplayMap, modifierDisplayMap } from "@/keyboardMappings";
|
|
||||||
import notifications from "@/notifications";
|
import notifications from "@/notifications";
|
||||||
import { ConfirmDialog } from "@/components/ConfirmDialog";
|
import { ConfirmDialog } from "@/components/ConfirmDialog";
|
||||||
import LoadingSpinner from "@/components/LoadingSpinner";
|
import LoadingSpinner from "@/components/LoadingSpinner";
|
||||||
|
import useKeyboardLayout from "@/hooks/useKeyboardLayout";
|
||||||
|
|
||||||
const normalizeSortOrders = (macros: KeySequence[]): KeySequence[] => {
|
const normalizeSortOrders = (macros: KeySequence[]): KeySequence[] => {
|
||||||
return macros.map((macro, index) => ({
|
return macros.map((macro, index) => ({
|
||||||
|
@ -35,6 +35,7 @@ export default function SettingsMacrosRoute() {
|
||||||
const [actionLoadingId, setActionLoadingId] = useState<string | null>(null);
|
const [actionLoadingId, setActionLoadingId] = useState<string | null>(null);
|
||||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||||
const [macroToDelete, setMacroToDelete] = useState<KeySequence | null>(null);
|
const [macroToDelete, setMacroToDelete] = useState<KeySequence | null>(null);
|
||||||
|
const { selectedKeyboard } = useKeyboardLayout();
|
||||||
|
|
||||||
const isMaxMacrosReached = useMemo(
|
const isMaxMacrosReached = useMemo(
|
||||||
() => macros.length >= MAX_TOTAL_MACROS,
|
() => macros.length >= MAX_TOTAL_MACROS,
|
||||||
|
@ -185,7 +186,7 @@ export default function SettingsMacrosRoute() {
|
||||||
step.modifiers.map((modifier, idx) => (
|
step.modifiers.map((modifier, idx) => (
|
||||||
<Fragment key={`mod-${idx}`}>
|
<Fragment key={`mod-${idx}`}>
|
||||||
<span className="font-medium text-slate-600 dark:text-slate-200">
|
<span className="font-medium text-slate-600 dark:text-slate-200">
|
||||||
{modifierDisplayMap[modifier] || modifier}
|
{selectedKeyboard.modifierDisplayMap[modifier] || modifier}
|
||||||
</span>
|
</span>
|
||||||
{idx < step.modifiers.length - 1 && (
|
{idx < step.modifiers.length - 1 && (
|
||||||
<span className="text-slate-400 dark:text-slate-600">
|
<span className="text-slate-400 dark:text-slate-600">
|
||||||
|
@ -210,7 +211,7 @@ export default function SettingsMacrosRoute() {
|
||||||
step.keys.map((key, idx) => (
|
step.keys.map((key, idx) => (
|
||||||
<Fragment key={`key-${idx}`}>
|
<Fragment key={`key-${idx}`}>
|
||||||
<span className="font-medium text-blue-600 dark:text-blue-400">
|
<span className="font-medium text-blue-600 dark:text-blue-400">
|
||||||
{keyDisplayMap[key] || key}
|
{selectedKeyboard.keyDisplayMap[key] || key}
|
||||||
</span>
|
</span>
|
||||||
{idx < step.keys.length - 1 && (
|
{idx < step.keys.length - 1 && (
|
||||||
<span className="text-slate-400 dark:text-slate-600">
|
<span className="text-slate-400 dark:text-slate-600">
|
||||||
|
@ -297,8 +298,10 @@ export default function SettingsMacrosRoute() {
|
||||||
actionLoadingId,
|
actionLoadingId,
|
||||||
handleDeleteMacro,
|
handleDeleteMacro,
|
||||||
handleMoveMacro,
|
handleMoveMacro,
|
||||||
|
selectedKeyboard.modifierDisplayMap,
|
||||||
|
selectedKeyboard.keyDisplayMap,
|
||||||
handleDuplicateMacro,
|
handleDuplicateMacro,
|
||||||
navigate,
|
navigate
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -64,14 +64,11 @@ const jigglerOptions = [
|
||||||
type JigglerValues = (typeof jigglerOptions)[number]["value"] | "custom";
|
type JigglerValues = (typeof jigglerOptions)[number]["value"] | "custom";
|
||||||
|
|
||||||
export default function SettingsMouseRoute() {
|
export default function SettingsMouseRoute() {
|
||||||
const hideCursor = useSettingsStore(state => state.isCursorHidden);
|
const {
|
||||||
const setHideCursor = useSettingsStore(state => state.setCursorVisibility);
|
isCursorHidden, setCursorVisibility,
|
||||||
|
mouseMode, setMouseMode,
|
||||||
const mouseMode = useSettingsStore(state => state.mouseMode);
|
scrollThrottling, setScrollThrottling
|
||||||
const setMouseMode = useSettingsStore(state => state.setMouseMode);
|
} = useSettingsStore();
|
||||||
|
|
||||||
const scrollThrottling = useSettingsStore(state => state.scrollThrottling);
|
|
||||||
const setScrollThrottling = useSettingsStore(state => state.setScrollThrottling);
|
|
||||||
|
|
||||||
const [selectedJigglerOption, setSelectedJigglerOption] =
|
const [selectedJigglerOption, setSelectedJigglerOption] =
|
||||||
useState<JigglerValues | null>(null);
|
useState<JigglerValues | null>(null);
|
||||||
|
@ -87,7 +84,7 @@ export default function SettingsMouseRoute() {
|
||||||
{ value: "100", label: "Very High" },
|
{ value: "100", label: "Very High" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const [send] = useJsonRpc();
|
const { send } = useJsonRpc();
|
||||||
|
|
||||||
const syncJigglerSettings = useCallback(() => {
|
const syncJigglerSettings = useCallback(() => {
|
||||||
send("getJigglerState", {}, resp => {
|
send("getJigglerState", {}, resp => {
|
||||||
|
@ -196,8 +193,8 @@ export default function SettingsMouseRoute() {
|
||||||
description="Hide the cursor when sending mouse movements"
|
description="Hide the cursor when sending mouse movements"
|
||||||
>
|
>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={hideCursor}
|
checked={isCursorHidden}
|
||||||
onChange={e => setHideCursor(e.target.checked)}
|
onChange={e => setCursorVisibility(e.target.checked)}
|
||||||
/>
|
/>
|
||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
|
|
||||||
|
|
|
@ -72,7 +72,7 @@ export function LifeTimeLabel({ lifetime }: { lifetime: string }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SettingsNetworkRoute() {
|
export default function SettingsNetworkRoute() {
|
||||||
const [send] = useJsonRpc();
|
const { send } = useJsonRpc();
|
||||||
const [networkState, setNetworkState] = useNetworkStateStore(state => [
|
const [networkState, setNetworkState] = useNetworkStateStore(state => [
|
||||||
state,
|
state,
|
||||||
state.setNetworkState,
|
state.setNetworkState,
|
||||||
|
@ -106,11 +106,12 @@ export default function SettingsNetworkRoute() {
|
||||||
setNetworkSettingsLoaded(false);
|
setNetworkSettingsLoaded(false);
|
||||||
send("getNetworkSettings", {}, resp => {
|
send("getNetworkSettings", {}, resp => {
|
||||||
if ("error" in resp) return;
|
if ("error" in resp) return;
|
||||||
console.log(resp.result);
|
const networkSettings = resp.result as NetworkSettings;
|
||||||
setNetworkSettings(resp.result as NetworkSettings);
|
console.debug("Network settings: ", networkSettings);
|
||||||
|
setNetworkSettings(networkSettings);
|
||||||
|
|
||||||
if (!firstNetworkSettings.current) {
|
if (!firstNetworkSettings.current) {
|
||||||
firstNetworkSettings.current = resp.result as NetworkSettings;
|
firstNetworkSettings.current = networkSettings;
|
||||||
}
|
}
|
||||||
setNetworkSettingsLoaded(true);
|
setNetworkSettingsLoaded(true);
|
||||||
});
|
});
|
||||||
|
@ -119,8 +120,9 @@ export default function SettingsNetworkRoute() {
|
||||||
const getNetworkState = useCallback(() => {
|
const getNetworkState = useCallback(() => {
|
||||||
send("getNetworkState", {}, resp => {
|
send("getNetworkState", {}, resp => {
|
||||||
if ("error" in resp) return;
|
if ("error" in resp) return;
|
||||||
console.log(resp.result);
|
const networkState = resp.result as NetworkState;
|
||||||
setNetworkState(resp.result as NetworkState);
|
console.debug("Network state:", networkState);
|
||||||
|
setNetworkState(networkState);
|
||||||
});
|
});
|
||||||
}, [send, setNetworkState]);
|
}, [send, setNetworkState]);
|
||||||
|
|
||||||
|
@ -136,9 +138,10 @@ export default function SettingsNetworkRoute() {
|
||||||
setNetworkSettingsLoaded(true);
|
setNetworkSettingsLoaded(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const networkSettings = resp.result as NetworkSettings;
|
||||||
// We need to update the firstNetworkSettings ref to the new settings so we can use it to determine if the settings have changed
|
// We need to update the firstNetworkSettings ref to the new settings so we can use it to determine if the settings have changed
|
||||||
firstNetworkSettings.current = resp.result as NetworkSettings;
|
firstNetworkSettings.current = networkSettings;
|
||||||
setNetworkSettings(resp.result as NetworkSettings);
|
setNetworkSettings(networkSettings);
|
||||||
getNetworkState();
|
getNetworkState();
|
||||||
setNetworkSettingsLoaded(true);
|
setNetworkSettingsLoaded(true);
|
||||||
notifications.success("Network settings saved");
|
notifications.success("Network settings saved");
|
||||||
|
|
|
@ -17,19 +17,16 @@ import { useResizeObserver } from "usehooks-ts";
|
||||||
|
|
||||||
import Card from "@/components/Card";
|
import Card from "@/components/Card";
|
||||||
import { LinkButton } from "@/components/Button";
|
import { LinkButton } from "@/components/Button";
|
||||||
|
import { FeatureFlag } from "@/components/FeatureFlag";
|
||||||
import LoadingSpinner from "@/components/LoadingSpinner";
|
import LoadingSpinner from "@/components/LoadingSpinner";
|
||||||
import { useUiStore } from "@/hooks/stores";
|
import { useUiStore } from "@/hooks/stores";
|
||||||
import useKeyboard from "@/hooks/useKeyboard";
|
|
||||||
|
|
||||||
import { FeatureFlag } from "../components/FeatureFlag";
|
|
||||||
import { cx } from "../cva.config";
|
import { cx } from "../cva.config";
|
||||||
|
|
||||||
|
|
||||||
/* TODO: Migrate to using URLs instead of the global state. To simplify the refactoring, we'll keep the global state for now. */
|
/* TODO: Migrate to using URLs instead of the global state. To simplify the refactoring, we'll keep the global state for now. */
|
||||||
export default function SettingsRoute() {
|
export default function SettingsRoute() {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const setDisableVideoFocusTrap = useUiStore(state => state.setDisableVideoFocusTrap);
|
const { setDisableVideoFocusTrap } = useUiStore();
|
||||||
const { sendKeyboardEvent } = useKeyboard();
|
|
||||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||||
const [showLeftGradient, setShowLeftGradient] = useState(false);
|
const [showLeftGradient, setShowLeftGradient] = useState(false);
|
||||||
const [showRightGradient, setShowRightGradient] = useState(false);
|
const [showRightGradient, setShowRightGradient] = useState(false);
|
||||||
|
@ -65,21 +62,14 @@ export default function SettingsRoute() {
|
||||||
}, [width]);
|
}, [width]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// disable focus trap
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// Reset keyboard state. Incase the user is pressing a key while enabling the sidebar
|
|
||||||
sendKeyboardEvent([], []);
|
|
||||||
setDisableVideoFocusTrap(true);
|
setDisableVideoFocusTrap(true);
|
||||||
// For some reason, the focus trap is not disabled immediately
|
}, 500);
|
||||||
// so we need to blur the active element
|
|
||||||
(document.activeElement as HTMLElement)?.blur();
|
|
||||||
console.log("Just disabled focus trap");
|
|
||||||
}, 300);
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
setDisableVideoFocusTrap(false);
|
setDisableVideoFocusTrap(false);
|
||||||
};
|
};
|
||||||
}, [sendKeyboardEvent, setDisableVideoFocusTrap]);
|
}, [setDisableVideoFocusTrap]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="pointer-events-auto relative mx-auto max-w-4xl translate-x-0 transform text-left dark:text-white">
|
<div className="pointer-events-auto relative mx-auto max-w-4xl translate-x-0 transform text-left dark:text-white">
|
||||||
|
|
|
@ -41,18 +41,17 @@ const streamQualityOptions = [
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function SettingsVideoRoute() {
|
export default function SettingsVideoRoute() {
|
||||||
const [send] = useJsonRpc();
|
const { send } = useJsonRpc();
|
||||||
const [streamQuality, setStreamQuality] = useState("1");
|
const [streamQuality, setStreamQuality] = useState("1");
|
||||||
const [customEdidValue, setCustomEdidValue] = useState<string | null>(null);
|
const [customEdidValue, setCustomEdidValue] = useState<string | null>(null);
|
||||||
const [edid, setEdid] = useState<string | null>(null);
|
const [edid, setEdid] = useState<string | null>(null);
|
||||||
|
|
||||||
// Video enhancement settings from store
|
// Video enhancement settings from store
|
||||||
const videoSaturation = useSettingsStore(state => state.videoSaturation);
|
const {
|
||||||
const setVideoSaturation = useSettingsStore(state => state.setVideoSaturation);
|
videoSaturation, setVideoSaturation,
|
||||||
const videoBrightness = useSettingsStore(state => state.videoBrightness);
|
videoBrightness, setVideoBrightness,
|
||||||
const setVideoBrightness = useSettingsStore(state => state.setVideoBrightness);
|
videoContrast, setVideoContrast
|
||||||
const videoContrast = useSettingsStore(state => state.videoContrast);
|
} = useSettingsStore();
|
||||||
const setVideoContrast = useSettingsStore(state => state.setVideoContrast);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
send("getStreamQualityFactor", {}, resp => {
|
send("getStreamQualityFactor", {}, resp => {
|
||||||
|
|
|
@ -18,10 +18,11 @@ import useWebSocket from "react-use-websocket";
|
||||||
|
|
||||||
import { cx } from "@/cva.config";
|
import { cx } from "@/cva.config";
|
||||||
import {
|
import {
|
||||||
HidState,
|
|
||||||
KeyboardLedState,
|
KeyboardLedState,
|
||||||
|
KeysDownState,
|
||||||
NetworkState,
|
NetworkState,
|
||||||
UpdateState,
|
OtaState,
|
||||||
|
USBStates,
|
||||||
useDeviceStore,
|
useDeviceStore,
|
||||||
useHidStore,
|
useHidStore,
|
||||||
useMountMediaStore,
|
useMountMediaStore,
|
||||||
|
@ -37,7 +38,7 @@ import WebRTCVideo from "@components/WebRTCVideo";
|
||||||
import { checkAuth, isInCloud, isOnDevice } from "@/main";
|
import { checkAuth, isInCloud, isOnDevice } from "@/main";
|
||||||
import DashboardNavbar from "@components/Header";
|
import DashboardNavbar from "@components/Header";
|
||||||
import ConnectionStatsSidebar from "@/components/sidebar/connectionStats";
|
import ConnectionStatsSidebar from "@/components/sidebar/connectionStats";
|
||||||
import { JsonRpcRequest, useJsonRpc } from "@/hooks/useJsonRpc";
|
import { JsonRpcRequest, JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
||||||
import Terminal from "@components/Terminal";
|
import Terminal from "@components/Terminal";
|
||||||
import { CLOUD_API, DEVICE_API } from "@/ui.config";
|
import { CLOUD_API, DEVICE_API } from "@/ui.config";
|
||||||
|
|
||||||
|
@ -127,22 +128,22 @@ export default function KvmIdRoute() {
|
||||||
const authMode = "authMode" in loaderResp ? loaderResp.authMode : null;
|
const authMode = "authMode" in loaderResp ? loaderResp.authMode : null;
|
||||||
|
|
||||||
const params = useParams() as { id: string };
|
const params = useParams() as { id: string };
|
||||||
const sidebarView = useUiStore(state => state.sidebarView);
|
const { sidebarView, setSidebarView, disableVideoFocusTrap } = useUiStore();
|
||||||
const [queryParams, setQueryParams] = useSearchParams();
|
const [ queryParams, setQueryParams ] = useSearchParams();
|
||||||
|
|
||||||
|
const {
|
||||||
|
peerConnection, setPeerConnection,
|
||||||
|
peerConnectionState, setPeerConnectionState,
|
||||||
|
diskChannel, setDiskChannel,
|
||||||
|
setMediaStream,
|
||||||
|
setRpcDataChannel,
|
||||||
|
isTurnServerInUse, setTurnServerInUse,
|
||||||
|
rpcDataChannel,
|
||||||
|
setTransceiver
|
||||||
|
} = useRTCStore();
|
||||||
|
|
||||||
const setIsTurnServerInUse = useRTCStore(state => state.setTurnServerInUse);
|
|
||||||
const peerConnection = useRTCStore(state => state.peerConnection);
|
|
||||||
const setPeerConnectionState = useRTCStore(state => state.setPeerConnectionState);
|
|
||||||
const peerConnectionState = useRTCStore(state => state.peerConnectionState);
|
|
||||||
const setMediaMediaStream = useRTCStore(state => state.setMediaStream);
|
|
||||||
const setPeerConnection = useRTCStore(state => state.setPeerConnection);
|
|
||||||
const setDiskChannel = useRTCStore(state => state.setDiskChannel);
|
|
||||||
const setRpcDataChannel = useRTCStore(state => state.setRpcDataChannel);
|
|
||||||
const setTransceiver = useRTCStore(state => state.setTransceiver);
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
const isLegacySignalingEnabled = useRef(false);
|
const isLegacySignalingEnabled = useRef(false);
|
||||||
|
|
||||||
const [connectionFailed, setConnectionFailed] = useState(false);
|
const [connectionFailed, setConnectionFailed] = useState(false);
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
@ -211,7 +212,7 @@ export default function KvmIdRoute() {
|
||||||
clearInterval(checkInterval);
|
clearInterval(checkInterval);
|
||||||
setLoadingMessage("Connection established");
|
setLoadingMessage("Connection established");
|
||||||
} else if (attempts >= 10) {
|
} else if (attempts >= 10) {
|
||||||
console.log(
|
console.warn(
|
||||||
"[setRemoteSessionDescription] Failed to establish connection after 10 attempts",
|
"[setRemoteSessionDescription] Failed to establish connection after 10 attempts",
|
||||||
{
|
{
|
||||||
connectionState: pc.connectionState,
|
connectionState: pc.connectionState,
|
||||||
|
@ -247,27 +248,27 @@ export default function KvmIdRoute() {
|
||||||
reconnectAttempts: 15,
|
reconnectAttempts: 15,
|
||||||
reconnectInterval: 1000,
|
reconnectInterval: 1000,
|
||||||
onReconnectStop: () => {
|
onReconnectStop: () => {
|
||||||
console.log("Reconnect stopped");
|
console.debug("Reconnect stopped");
|
||||||
cleanupAndStopReconnecting();
|
cleanupAndStopReconnecting();
|
||||||
},
|
},
|
||||||
|
|
||||||
shouldReconnect(event) {
|
shouldReconnect(event) {
|
||||||
console.log("[Websocket] shouldReconnect", event);
|
console.debug("[Websocket] shouldReconnect", event);
|
||||||
// TODO: Why true?
|
// TODO: Why true?
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
|
||||||
onClose(event) {
|
onClose(event) {
|
||||||
console.log("[Websocket] onClose", event);
|
console.debug("[Websocket] onClose", event);
|
||||||
// We don't want to close everything down, we wait for the reconnect to stop instead
|
// We don't want to close everything down, we wait for the reconnect to stop instead
|
||||||
},
|
},
|
||||||
|
|
||||||
onError(event) {
|
onError(event) {
|
||||||
console.log("[Websocket] onError", event);
|
console.error("[Websocket] onError", event);
|
||||||
// We don't want to close everything down, we wait for the reconnect to stop instead
|
// We don't want to close everything down, we wait for the reconnect to stop instead
|
||||||
},
|
},
|
||||||
onOpen() {
|
onOpen() {
|
||||||
console.log("[Websocket] onOpen");
|
console.debug("[Websocket] onOpen");
|
||||||
},
|
},
|
||||||
|
|
||||||
onMessage: message => {
|
onMessage: message => {
|
||||||
|
@ -289,8 +290,8 @@ export default function KvmIdRoute() {
|
||||||
const parsedMessage = JSON.parse(message.data);
|
const parsedMessage = JSON.parse(message.data);
|
||||||
if (parsedMessage.type === "device-metadata") {
|
if (parsedMessage.type === "device-metadata") {
|
||||||
const { deviceVersion } = parsedMessage.data;
|
const { deviceVersion } = parsedMessage.data;
|
||||||
console.log("[Websocket] Received device-metadata message");
|
console.debug("[Websocket] Received device-metadata message");
|
||||||
console.log("[Websocket] Device version", deviceVersion);
|
console.debug("[Websocket] Device version", deviceVersion);
|
||||||
// If the device version is not set, we can assume the device is using the legacy signaling
|
// If the device version is not set, we can assume the device is using the legacy signaling
|
||||||
if (!deviceVersion) {
|
if (!deviceVersion) {
|
||||||
console.log("[Websocket] Device is using legacy signaling");
|
console.log("[Websocket] Device is using legacy signaling");
|
||||||
|
@ -308,7 +309,7 @@ export default function KvmIdRoute() {
|
||||||
|
|
||||||
if (!peerConnection) return;
|
if (!peerConnection) return;
|
||||||
if (parsedMessage.type === "answer") {
|
if (parsedMessage.type === "answer") {
|
||||||
console.log("[Websocket] Received answer");
|
console.debug("[Websocket] Received answer");
|
||||||
const readyForOffer =
|
const readyForOffer =
|
||||||
// If we're making an offer, we don't want to accept an answer
|
// If we're making an offer, we don't want to accept an answer
|
||||||
!makingOffer &&
|
!makingOffer &&
|
||||||
|
@ -322,7 +323,7 @@ export default function KvmIdRoute() {
|
||||||
|
|
||||||
// Set so we don't accept an answer while we're setting the remote description
|
// Set so we don't accept an answer while we're setting the remote description
|
||||||
isSettingRemoteAnswerPending.current = parsedMessage.type === "answer";
|
isSettingRemoteAnswerPending.current = parsedMessage.type === "answer";
|
||||||
console.log(
|
console.debug(
|
||||||
"[Websocket] Setting remote answer pending",
|
"[Websocket] Setting remote answer pending",
|
||||||
isSettingRemoteAnswerPending.current,
|
isSettingRemoteAnswerPending.current,
|
||||||
);
|
);
|
||||||
|
@ -338,7 +339,7 @@ export default function KvmIdRoute() {
|
||||||
// Reset the remote answer pending flag
|
// Reset the remote answer pending flag
|
||||||
isSettingRemoteAnswerPending.current = false;
|
isSettingRemoteAnswerPending.current = false;
|
||||||
} else if (parsedMessage.type === "new-ice-candidate") {
|
} else if (parsedMessage.type === "new-ice-candidate") {
|
||||||
console.log("[Websocket] Received new-ice-candidate");
|
console.debug("[Websocket] Received new-ice-candidate");
|
||||||
const candidate = parsedMessage.data;
|
const candidate = parsedMessage.data;
|
||||||
peerConnection.addIceCandidate(candidate);
|
peerConnection.addIceCandidate(candidate);
|
||||||
}
|
}
|
||||||
|
@ -384,7 +385,7 @@ export default function KvmIdRoute() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("Successfully got Remote Session Description. Setting.");
|
console.debug("Successfully got Remote Session Description. Setting.");
|
||||||
setLoadingMessage("Setting remote session description...");
|
setLoadingMessage("Setting remote session description...");
|
||||||
|
|
||||||
const decodedSd = atob(json.sd);
|
const decodedSd = atob(json.sd);
|
||||||
|
@ -395,13 +396,13 @@ export default function KvmIdRoute() {
|
||||||
);
|
);
|
||||||
|
|
||||||
const setupPeerConnection = useCallback(async () => {
|
const setupPeerConnection = useCallback(async () => {
|
||||||
console.log("[setupPeerConnection] Setting up peer connection");
|
console.debug("[setupPeerConnection] Setting up peer connection");
|
||||||
setConnectionFailed(false);
|
setConnectionFailed(false);
|
||||||
setLoadingMessage("Connecting to device...");
|
setLoadingMessage("Connecting to device...");
|
||||||
|
|
||||||
let pc: RTCPeerConnection;
|
let pc: RTCPeerConnection;
|
||||||
try {
|
try {
|
||||||
console.log("[setupPeerConnection] Creating peer connection");
|
console.debug("[setupPeerConnection] Creating peer connection");
|
||||||
setLoadingMessage("Creating peer connection...");
|
setLoadingMessage("Creating peer connection...");
|
||||||
pc = new RTCPeerConnection({
|
pc = new RTCPeerConnection({
|
||||||
// We only use STUN or TURN servers if we're in the cloud
|
// We only use STUN or TURN servers if we're in the cloud
|
||||||
|
@ -411,7 +412,7 @@ export default function KvmIdRoute() {
|
||||||
});
|
});
|
||||||
|
|
||||||
setPeerConnectionState(pc.connectionState);
|
setPeerConnectionState(pc.connectionState);
|
||||||
console.log("[setupPeerConnection] Peer connection created", pc);
|
console.debug("[setupPeerConnection] Peer connection created", pc);
|
||||||
setLoadingMessage("Setting up connection to device...");
|
setLoadingMessage("Setting up connection to device...");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`[setupPeerConnection] Error creating peer connection: ${e}`);
|
console.error(`[setupPeerConnection] Error creating peer connection: ${e}`);
|
||||||
|
@ -423,13 +424,13 @@ export default function KvmIdRoute() {
|
||||||
|
|
||||||
// Set up event listeners and data channels
|
// Set up event listeners and data channels
|
||||||
pc.onconnectionstatechange = () => {
|
pc.onconnectionstatechange = () => {
|
||||||
console.log("[setupPeerConnection] Connection state changed", pc.connectionState);
|
console.debug("[setupPeerConnection] Connection state changed", pc.connectionState);
|
||||||
setPeerConnectionState(pc.connectionState);
|
setPeerConnectionState(pc.connectionState);
|
||||||
};
|
};
|
||||||
|
|
||||||
pc.onnegotiationneeded = async () => {
|
pc.onnegotiationneeded = async () => {
|
||||||
try {
|
try {
|
||||||
console.log("[setupPeerConnection] Creating offer");
|
console.debug("[setupPeerConnection] Creating offer");
|
||||||
makingOffer.current = true;
|
makingOffer.current = true;
|
||||||
|
|
||||||
const offer = await pc.createOffer();
|
const offer = await pc.createOffer();
|
||||||
|
@ -439,7 +440,7 @@ export default function KvmIdRoute() {
|
||||||
if (isNewSignalingEnabled) {
|
if (isNewSignalingEnabled) {
|
||||||
sendWebRTCSignal("offer", { sd: sd });
|
sendWebRTCSignal("offer", { sd: sd });
|
||||||
} else {
|
} else {
|
||||||
console.log("Legacy signanling. Waiting for ICE Gathering to complete...");
|
console.log("Legacy signaling. Waiting for ICE Gathering to complete...");
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(
|
console.error(
|
||||||
|
@ -461,7 +462,7 @@ export default function KvmIdRoute() {
|
||||||
pc.onicegatheringstatechange = event => {
|
pc.onicegatheringstatechange = event => {
|
||||||
const pc = event.currentTarget as RTCPeerConnection;
|
const pc = event.currentTarget as RTCPeerConnection;
|
||||||
if (pc.iceGatheringState === "complete") {
|
if (pc.iceGatheringState === "complete") {
|
||||||
console.log("ICE Gathering completed");
|
console.debug("ICE Gathering completed");
|
||||||
setLoadingMessage("ICE Gathering completed");
|
setLoadingMessage("ICE Gathering completed");
|
||||||
|
|
||||||
if (isLegacySignalingEnabled.current) {
|
if (isLegacySignalingEnabled.current) {
|
||||||
|
@ -469,13 +470,13 @@ export default function KvmIdRoute() {
|
||||||
legacyHTTPSignaling(pc);
|
legacyHTTPSignaling(pc);
|
||||||
}
|
}
|
||||||
} else if (pc.iceGatheringState === "gathering") {
|
} else if (pc.iceGatheringState === "gathering") {
|
||||||
console.log("ICE Gathering Started");
|
console.debug("ICE Gathering Started");
|
||||||
setLoadingMessage("Gathering ICE candidates...");
|
setLoadingMessage("Gathering ICE candidates...");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
pc.ontrack = function (event) {
|
pc.ontrack = function (event) {
|
||||||
setMediaMediaStream(event.streams[0]);
|
setMediaStream(event.streams[0]);
|
||||||
};
|
};
|
||||||
|
|
||||||
setTransceiver(pc.addTransceiver("video", { direction: "recvonly" }));
|
setTransceiver(pc.addTransceiver("video", { direction: "recvonly" }));
|
||||||
|
@ -497,7 +498,7 @@ export default function KvmIdRoute() {
|
||||||
legacyHTTPSignaling,
|
legacyHTTPSignaling,
|
||||||
sendWebRTCSignal,
|
sendWebRTCSignal,
|
||||||
setDiskChannel,
|
setDiskChannel,
|
||||||
setMediaMediaStream,
|
setMediaStream,
|
||||||
setPeerConnection,
|
setPeerConnection,
|
||||||
setPeerConnectionState,
|
setPeerConnectionState,
|
||||||
setRpcDataChannel,
|
setRpcDataChannel,
|
||||||
|
@ -506,15 +507,13 @@ export default function KvmIdRoute() {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (peerConnectionState === "failed") {
|
if (peerConnectionState === "failed") {
|
||||||
console.log("Connection failed, closing peer connection");
|
console.warn("Connection failed, closing peer connection");
|
||||||
cleanupAndStopReconnecting();
|
cleanupAndStopReconnecting();
|
||||||
}
|
}
|
||||||
}, [peerConnectionState, cleanupAndStopReconnecting]);
|
}, [peerConnectionState, cleanupAndStopReconnecting]);
|
||||||
|
|
||||||
// Cleanup effect
|
// Cleanup effect
|
||||||
const clearInboundRtpStats = useRTCStore(state => state.clearInboundRtpStats);
|
const { clearInboundRtpStats, clearCandidatePairStats } = useRTCStore();
|
||||||
const clearCandidatePairStats = useRTCStore(state => state.clearCandidatePairStats);
|
|
||||||
const setSidebarView = useUiStore(state => state.setSidebarView);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
|
@ -545,11 +544,10 @@ export default function KvmIdRoute() {
|
||||||
if (!lastRemoteStat?.length) return;
|
if (!lastRemoteStat?.length) return;
|
||||||
const remoteCandidateIsUsingTurn = lastRemoteStat[1].candidateType === "relay"; // [0] is the timestamp, which we don't care about here
|
const remoteCandidateIsUsingTurn = lastRemoteStat[1].candidateType === "relay"; // [0] is the timestamp, which we don't care about here
|
||||||
|
|
||||||
setIsTurnServerInUse(localCandidateIsUsingTurn || remoteCandidateIsUsingTurn);
|
setTurnServerInUse(localCandidateIsUsingTurn || remoteCandidateIsUsingTurn);
|
||||||
}, [peerConnectionState, setIsTurnServerInUse]);
|
}, [peerConnectionState, setTurnServerInUse]);
|
||||||
|
|
||||||
// TURN server usage reporting
|
// TURN server usage reporting
|
||||||
const isTurnServerInUse = useRTCStore(state => state.isTurnServerInUse);
|
|
||||||
const lastBytesReceived = useRef<number>(0);
|
const lastBytesReceived = useRef<number>(0);
|
||||||
const lastBytesSent = useRef<number>(0);
|
const lastBytesSent = useRef<number>(0);
|
||||||
|
|
||||||
|
@ -582,15 +580,13 @@ export default function KvmIdRoute() {
|
||||||
});
|
});
|
||||||
}, 10000);
|
}, 10000);
|
||||||
|
|
||||||
const setNetworkState = useNetworkStateStore(state => state.setNetworkState);
|
const { setNetworkState} = useNetworkStateStore();
|
||||||
|
const { setHdmiState } = useVideoStore();
|
||||||
const setUsbState = useHidStore(state => state.setUsbState);
|
const {
|
||||||
const setHdmiState = useVideoStore(state => state.setHdmiState);
|
keyboardLedState, setKeyboardLedState,
|
||||||
|
keysDownState, setKeysDownState, setUsbState,
|
||||||
const keyboardLedState = useHidStore(state => state.keyboardLedState);
|
setkeyPressReportApiAvailable
|
||||||
const setKeyboardLedState = useHidStore(state => state.setKeyboardLedState);
|
} = useHidStore();
|
||||||
|
|
||||||
const setKeyboardLedStateSyncAvailable = useHidStore(state => state.setKeyboardLedStateSyncAvailable);
|
|
||||||
|
|
||||||
const [hasUpdated, setHasUpdated] = useState(false);
|
const [hasUpdated, setHasUpdated] = useState(false);
|
||||||
const { navigateTo } = useDeviceUiNavigation();
|
const { navigateTo } = useDeviceUiNavigation();
|
||||||
|
@ -601,27 +597,38 @@ export default function KvmIdRoute() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resp.method === "usbState") {
|
if (resp.method === "usbState") {
|
||||||
setUsbState(resp.params as unknown as HidState["usbState"]);
|
const usbState = resp.params as unknown as USBStates;
|
||||||
|
console.debug("Setting USB state", usbState);
|
||||||
|
setUsbState(usbState);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resp.method === "videoInputState") {
|
if (resp.method === "videoInputState") {
|
||||||
setHdmiState(resp.params as Parameters<VideoState["setHdmiState"]>[0]);
|
const hdmiState = resp.params as Parameters<VideoState["setHdmiState"]>[0];
|
||||||
|
console.debug("Setting HDMI state", hdmiState);
|
||||||
|
setHdmiState(hdmiState);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resp.method === "networkState") {
|
if (resp.method === "networkState") {
|
||||||
console.log("Setting network state", resp.params);
|
console.debug("Setting network state", resp.params);
|
||||||
setNetworkState(resp.params as NetworkState);
|
setNetworkState(resp.params as NetworkState);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resp.method === "keyboardLedState") {
|
if (resp.method === "keyboardLedState") {
|
||||||
const ledState = resp.params as KeyboardLedState;
|
const ledState = resp.params as KeyboardLedState;
|
||||||
console.log("Setting keyboard led state", ledState);
|
console.debug("Setting keyboard led state", ledState);
|
||||||
setKeyboardLedState(ledState);
|
setKeyboardLedState(ledState);
|
||||||
setKeyboardLedStateSyncAvailable(true);
|
}
|
||||||
|
|
||||||
|
if (resp.method === "keysDownState") {
|
||||||
|
const downState = resp.params as KeysDownState;
|
||||||
|
console.debug("Setting key down state:", downState);
|
||||||
|
setKeysDownState(downState);
|
||||||
|
setkeyPressReportApiAvailable(true); // if they returned a keyDownState, we know they also support keyPressReport
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resp.method === "otaState") {
|
if (resp.method === "otaState") {
|
||||||
const otaState = resp.params as UpdateState["otaState"];
|
const otaState = resp.params as OtaState;
|
||||||
|
console.debug("Setting OTA state", otaState);
|
||||||
setOtaState(otaState);
|
setOtaState(otaState);
|
||||||
|
|
||||||
if (otaState.updating === true) {
|
if (otaState.updating === true) {
|
||||||
|
@ -645,39 +652,67 @@ export default function KvmIdRoute() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const rpcDataChannel = useRTCStore(state => state.rpcDataChannel);
|
const { send } = useJsonRpc(onJsonRpcRequest);
|
||||||
const [send] = useJsonRpc(onJsonRpcRequest);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (rpcDataChannel?.readyState !== "open") return;
|
if (rpcDataChannel?.readyState !== "open") return;
|
||||||
send("getVideoState", {}, resp => {
|
console.log("Requesting video state");
|
||||||
|
send("getVideoState", {}, (resp: JsonRpcResponse) => {
|
||||||
if ("error" in resp) return;
|
if ("error" in resp) return;
|
||||||
setHdmiState(resp.result as Parameters<VideoState["setHdmiState"]>[0]);
|
const hdmiState = resp.result as Parameters<VideoState["setHdmiState"]>[0];
|
||||||
|
console.debug("Setting HDMI state", hdmiState);
|
||||||
|
setHdmiState(hdmiState);
|
||||||
});
|
});
|
||||||
}, [rpcDataChannel?.readyState, send, setHdmiState]);
|
}, [rpcDataChannel?.readyState, send, setHdmiState]);
|
||||||
|
|
||||||
|
const [needLedState, setNeedLedState] = useState(true);
|
||||||
|
|
||||||
// request keyboard led state from the device
|
// request keyboard led state from the device
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (rpcDataChannel?.readyState !== "open") return;
|
if (rpcDataChannel?.readyState !== "open") return;
|
||||||
if (keyboardLedState !== undefined) return;
|
if (!needLedState) return;
|
||||||
console.log("Requesting keyboard led state");
|
console.log("Requesting keyboard led state");
|
||||||
|
|
||||||
send("getKeyboardLedState", {}, resp => {
|
send("getKeyboardLedState", {}, (resp: JsonRpcResponse) => {
|
||||||
|
if ("error" in resp) {
|
||||||
|
console.error("Failed to get keyboard led state", resp.error);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
const ledState = resp.result as KeyboardLedState;
|
||||||
|
console.debug("Keyboard led state: ", ledState);
|
||||||
|
setKeyboardLedState(ledState);
|
||||||
|
}
|
||||||
|
setNeedLedState(false);
|
||||||
|
});
|
||||||
|
}, [rpcDataChannel?.readyState, send, setKeyboardLedState, keyboardLedState, needLedState]);
|
||||||
|
|
||||||
|
const [needKeyDownState, setNeedKeyDownState] = useState(true);
|
||||||
|
|
||||||
|
// request keyboard key down state from the device
|
||||||
|
useEffect(() => {
|
||||||
|
if (rpcDataChannel?.readyState !== "open") return;
|
||||||
|
if (!needKeyDownState) return;
|
||||||
|
console.log("Requesting keys down state");
|
||||||
|
|
||||||
|
send("getKeyDownState", {}, (resp: JsonRpcResponse) => {
|
||||||
if ("error" in resp) {
|
if ("error" in resp) {
|
||||||
// -32601 means the method is not supported
|
// -32601 means the method is not supported
|
||||||
if (resp.error.code === -32601) {
|
if (resp.error.code === -32601) {
|
||||||
setKeyboardLedStateSyncAvailable(false);
|
// if we don't support key down state, we know key press is also not available
|
||||||
console.error("Failed to get keyboard led state, disabling sync", resp.error);
|
console.warn("Failed to get key down state, switching to old-school", resp.error);
|
||||||
|
setkeyPressReportApiAvailable(false);
|
||||||
} else {
|
} else {
|
||||||
console.error("Failed to get keyboard led state", resp.error);
|
console.error("Failed to get key down state", resp.error);
|
||||||
}
|
}
|
||||||
return;
|
} else {
|
||||||
|
const downState = resp.result as KeysDownState;
|
||||||
|
console.debug("Keyboard key down state", downState);
|
||||||
|
setKeysDownState(downState);
|
||||||
|
setkeyPressReportApiAvailable(true); // if they returned a keyDownState, we know they also support keyPressReport
|
||||||
}
|
}
|
||||||
console.log("Keyboard led state", resp.result);
|
setNeedKeyDownState(false);
|
||||||
setKeyboardLedState(resp.result as KeyboardLedState);
|
|
||||||
setKeyboardLedStateSyncAvailable(true);
|
|
||||||
});
|
});
|
||||||
}, [rpcDataChannel?.readyState, send, setKeyboardLedState, setKeyboardLedStateSyncAvailable, keyboardLedState]);
|
}, [keysDownState, needKeyDownState, rpcDataChannel?.readyState, send, setkeyPressReportApiAvailable, setKeysDownState]);
|
||||||
|
|
||||||
// When the update is successful, we need to refresh the client javascript and show a success modal
|
// When the update is successful, we need to refresh the client javascript and show a success modal
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -686,14 +721,13 @@ export default function KvmIdRoute() {
|
||||||
}
|
}
|
||||||
}, [navigate, navigateTo, queryParams, setModalView, setQueryParams]);
|
}, [navigate, navigateTo, queryParams, setModalView, setQueryParams]);
|
||||||
|
|
||||||
const diskChannel = useRTCStore(state => state.diskChannel)!;
|
const { localFile } = useMountMediaStore();
|
||||||
const file = useMountMediaStore(state => state.localFile)!;
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!diskChannel || !file) return;
|
if (!diskChannel || !localFile) return;
|
||||||
diskChannel.onmessage = async e => {
|
diskChannel.onmessage = async e => {
|
||||||
console.log("Received", e.data);
|
console.debug("Received", e.data);
|
||||||
const data = JSON.parse(e.data);
|
const data = JSON.parse(e.data);
|
||||||
const blob = file.slice(data.start, data.end);
|
const blob = localFile.slice(data.start, data.end);
|
||||||
const buf = await blob.arrayBuffer();
|
const buf = await blob.arrayBuffer();
|
||||||
const header = new ArrayBuffer(16);
|
const header = new ArrayBuffer(16);
|
||||||
const headerView = new DataView(header);
|
const headerView = new DataView(header);
|
||||||
|
@ -704,11 +738,9 @@ export default function KvmIdRoute() {
|
||||||
fullData.set(new Uint8Array(buf), header.byteLength);
|
fullData.set(new Uint8Array(buf), header.byteLength);
|
||||||
diskChannel.send(fullData);
|
diskChannel.send(fullData);
|
||||||
};
|
};
|
||||||
}, [diskChannel, file]);
|
}, [diskChannel, localFile]);
|
||||||
|
|
||||||
// System update
|
// System update
|
||||||
const disableVideoFocusTrap = useUiStore(state => state.disableVideoFocusTrap);
|
|
||||||
|
|
||||||
const [kvmTerminal, setKvmTerminal] = useState<RTCDataChannel | null>(null);
|
const [kvmTerminal, setKvmTerminal] = useState<RTCDataChannel | null>(null);
|
||||||
const [serialConsole, setSerialConsole] = useState<RTCDataChannel | null>(null);
|
const [serialConsole, setSerialConsole] = useState<RTCDataChannel | null>(null);
|
||||||
|
|
||||||
|
@ -728,14 +760,12 @@ export default function KvmIdRoute() {
|
||||||
if (location.pathname !== "/other-session") navigateTo("/");
|
if (location.pathname !== "/other-session") navigateTo("/");
|
||||||
}, [navigateTo, location.pathname]);
|
}, [navigateTo, location.pathname]);
|
||||||
|
|
||||||
const appVersion = useDeviceStore(state => state.appVersion);
|
const { appVersion, setAppVersion, setSystemVersion} = useDeviceStore();
|
||||||
const setAppVersion = useDeviceStore(state => state.setAppVersion);
|
|
||||||
const setSystemVersion = useDeviceStore(state => state.setSystemVersion);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (appVersion) return;
|
if (appVersion) return;
|
||||||
|
|
||||||
send("getUpdateStatus", {}, async resp => {
|
send("getUpdateStatus", {}, async (resp: JsonRpcResponse) => {
|
||||||
if ("error" in resp) {
|
if ("error" in resp) {
|
||||||
notifications.error(`Failed to get device version: ${resp.error}`);
|
notifications.error(`Failed to get device version: ${resp.error}`);
|
||||||
return
|
return
|
||||||
|
@ -875,7 +905,7 @@ interface SidebarContainerProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
function SidebarContainer(props: SidebarContainerProps) {
|
function SidebarContainer(props: SidebarContainerProps) {
|
||||||
const { sidebarView }= props;
|
const { sidebarView } = props;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cx(
|
className={cx(
|
||||||
|
|
24
usb.go
24
usb.go
|
@ -31,21 +31,31 @@ func initUsbGadget() {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
gadget.SetOnKeysDownChange(func(state usbgadget.KeysDownState) {
|
||||||
|
if currentSession != nil {
|
||||||
|
writeJSONRPCEvent("keysDownState", state, currentSession)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// open the keyboard hid file to listen for keyboard events
|
// open the keyboard hid file to listen for keyboard events
|
||||||
if err := gadget.OpenKeyboardHidFile(); err != nil {
|
if err := gadget.OpenKeyboardHidFile(); err != nil {
|
||||||
usbLogger.Error().Err(err).Msg("failed to open keyboard hid file")
|
usbLogger.Error().Err(err).Msg("failed to open keyboard hid file")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func rpcKeyboardReport(modifier uint8, keys []uint8) error {
|
func rpcKeyboardReport(modifier byte, keys []byte) (usbgadget.KeysDownState, error) {
|
||||||
return gadget.KeyboardReport(modifier, keys)
|
return gadget.KeyboardReport(modifier, keys)
|
||||||
}
|
}
|
||||||
|
|
||||||
func rpcAbsMouseReport(x, y int, buttons uint8) error {
|
func rpcKeypressReport(key byte, press bool) (usbgadget.KeysDownState, error) {
|
||||||
|
return gadget.KeypressReport(key, press)
|
||||||
|
}
|
||||||
|
|
||||||
|
func rpcAbsMouseReport(x int, y int, buttons uint8) error {
|
||||||
return gadget.AbsMouseReport(x, y, buttons)
|
return gadget.AbsMouseReport(x, y, buttons)
|
||||||
}
|
}
|
||||||
|
|
||||||
func rpcRelMouseReport(dx, dy int8, buttons uint8) error {
|
func rpcRelMouseReport(dx int8, dy int8, buttons uint8) error {
|
||||||
return gadget.RelMouseReport(dx, dy, buttons)
|
return gadget.RelMouseReport(dx, dy, buttons)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,6 +67,10 @@ func rpcGetKeyboardLedState() (state usbgadget.KeyboardState) {
|
||||||
return gadget.GetKeyboardState()
|
return gadget.GetKeyboardState()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func rpcGetKeysDownState() (state usbgadget.KeysDownState) {
|
||||||
|
return gadget.GetKeysDownState()
|
||||||
|
}
|
||||||
|
|
||||||
var usbState = "unknown"
|
var usbState = "unknown"
|
||||||
|
|
||||||
func rpcGetUSBState() (state string) {
|
func rpcGetUSBState() (state string) {
|
||||||
|
@ -66,7 +80,7 @@ func rpcGetUSBState() (state string) {
|
||||||
func triggerUSBStateUpdate() {
|
func triggerUSBStateUpdate() {
|
||||||
go func() {
|
go func() {
|
||||||
if currentSession == nil {
|
if currentSession == nil {
|
||||||
usbLogger.Info().Msg("No active RPC session, skipping update state update")
|
usbLogger.Info().Msg("No active RPC session, skipping USB state update")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeJSONRPCEvent("usbState", usbState, currentSession)
|
writeJSONRPCEvent("usbState", usbState, currentSession)
|
||||||
|
@ -78,9 +92,9 @@ func checkUSBState() {
|
||||||
if newState == usbState {
|
if newState == usbState {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
usbLogger.Info().Str("from", usbState).Str("to", newState).Msg("USB state changed")
|
||||||
usbState = newState
|
usbState = newState
|
||||||
|
|
||||||
usbLogger.Info().Str("from", usbState).Str("to", newState).Msg("USB state changed")
|
|
||||||
requestDisplayUpdate(true)
|
requestDisplayUpdate(true)
|
||||||
triggerUSBStateUpdate()
|
triggerUSBStateUpdate()
|
||||||
}
|
}
|
||||||
|
|
|
@ -102,6 +102,7 @@ func newSession(config SessionConfig) (*Session, error) {
|
||||||
ICEServers: []webrtc.ICEServer{iceServer},
|
ICEServers: []webrtc.ICEServer{iceServer},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
scopedLogger.Warn().Err(err).Msg("Failed to create PeerConnection")
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
session := &Session{peerConnection: peerConnection}
|
session := &Session{peerConnection: peerConnection}
|
||||||
|
@ -133,11 +134,13 @@ func newSession(config SessionConfig) (*Session, error) {
|
||||||
|
|
||||||
session.VideoTrack, err = webrtc.NewTrackLocalStaticSample(webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeH264}, "video", "kvm")
|
session.VideoTrack, err = webrtc.NewTrackLocalStaticSample(webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeH264}, "video", "kvm")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
scopedLogger.Warn().Err(err).Msg("Failed to create VideoTrack")
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
rtpSender, err := peerConnection.AddTrack(session.VideoTrack)
|
rtpSender, err := peerConnection.AddTrack(session.VideoTrack)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
scopedLogger.Warn().Err(err).Msg("Failed to add VideoTrack to PeerConnection")
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -187,9 +190,10 @@ func newSession(config SessionConfig) (*Session, error) {
|
||||||
currentSession = nil
|
currentSession = nil
|
||||||
}
|
}
|
||||||
if session.shouldUmountVirtualMedia {
|
if session.shouldUmountVirtualMedia {
|
||||||
err := rpcUnmountImage()
|
if err := rpcUnmountImage(); err != nil {
|
||||||
scopedLogger.Warn().Err(err).Msg("unmount image failed on connection close")
|
scopedLogger.Warn().Err(err).Msg("unmount image failed on connection close")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if isConnected {
|
if isConnected {
|
||||||
isConnected = false
|
isConnected = false
|
||||||
actionSessions--
|
actionSessions--
|
||||||
|
|
2
wol.go
2
wol.go
|
@ -65,7 +65,7 @@ func createMagicPacket(mac net.HardwareAddr) []byte {
|
||||||
buf.Write(bytes.Repeat([]byte{0xFF}, 6))
|
buf.Write(bytes.Repeat([]byte{0xFF}, 6))
|
||||||
|
|
||||||
// Write the target MAC address 16 times
|
// Write the target MAC address 16 times
|
||||||
for i := 0; i < 16; i++ {
|
for range 16 {
|
||||||
_ = binary.Write(&buf, binary.BigEndian, mac)
|
_ = binary.Write(&buf, binary.BigEndian, mac)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue