diff --git a/config.go b/config.go index 537f85f..1fa56a7 100644 --- a/config.go +++ b/config.go @@ -114,7 +114,7 @@ var defaultConfig = &Config{ ActiveExtension: "", KeyboardMacros: []KeyboardMacro{}, DisplayRotation: "270", - KeyboardLayout: "en_US", + KeyboardLayout: "en-US", DisplayMaxBrightness: 64, DisplayDimAfterSec: 120, // 2 minutes DisplayOffAfterSec: 1800, // 30 minutes diff --git a/display.go b/display.go index 274bb8b..aab19fb 100644 --- a/display.go +++ b/display.go @@ -30,7 +30,7 @@ const ( // do not call this function directly, use switchToScreenIfDifferent instead // this function is not thread safe 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 { displayLogger.Warn().Err(err).Str("screen", screen).Msg("failed to switch to screen") return @@ -39,15 +39,15 @@ func switchToScreen(screen string) { } 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) { - 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) { - 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) { @@ -59,27 +59,27 @@ func lvObjShow(objName string) (*CtrlResponse, error) { } 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) { - 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) { - 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) { - 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) { - 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) { - 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) { diff --git a/internal/confparser/confparser.go b/internal/confparser/confparser.go index 5ccd1cb..aaa3968 100644 --- a/internal/confparser/confparser.go +++ b/internal/confparser/confparser.go @@ -16,22 +16,22 @@ import ( type FieldConfig struct { Name string Required bool - RequiredIf map[string]interface{} + RequiredIf map[string]any OneOf []string ValidateTypes []string - Defaults interface{} + Defaults any IsEmpty bool - CurrentValue interface{} + CurrentValue any TypeString string Delegated bool shouldUpdateValue bool } -func SetDefaultsAndValidate(config interface{}) error { +func SetDefaultsAndValidate(config any) error { 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 if reflect.TypeOf(config).Kind() != reflect.Ptr { return fmt.Errorf("config is not a pointer") @@ -55,7 +55,7 @@ func setDefaultsAndValidate(config interface{}, isRoot bool) error { Name: field.Name, OneOf: splitString(field.Tag.Get("one_of")), ValidateTypes: splitString(field.Tag.Get("validate_type")), - RequiredIf: make(map[string]interface{}), + RequiredIf: make(map[string]any), CurrentValue: fieldValue.Interface(), IsEmpty: false, TypeString: fieldType, @@ -142,8 +142,8 @@ func setDefaultsAndValidate(config interface{}, isRoot bool) error { // now check if the field has required_if requiredIf := field.Tag.Get("required_if") if requiredIf != "" { - requiredIfParts := strings.Split(requiredIf, ",") - for _, part := range requiredIfParts { + requiredIfParts := strings.SplitSeq(requiredIf, ",") + for part := range requiredIfParts { partVal := strings.SplitN(part, "=", 2) if len(partVal) != 2 { 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 } -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 for _, fieldConfig := range fields { if err := fieldConfig.validate(fields); err != nil { @@ -215,7 +215,7 @@ func (f *FieldConfig) validate(fields map[string]FieldConfig) error { return nil } -func (f *FieldConfig) populate(config interface{}) { +func (f *FieldConfig) populate(config any) { // update the field if it's not empty if !f.shouldUpdateValue { return diff --git a/internal/confparser/utils.go b/internal/confparser/utils.go index a46871e..36ee28b 100644 --- a/internal/confparser/utils.go +++ b/internal/confparser/utils.go @@ -16,7 +16,7 @@ func splitString(s string) []string { return strings.Split(s, ",") } -func toString(v interface{}) (string, error) { +func toString(v any) (string, error) { switch v := v.(type) { case string: return v, nil diff --git a/internal/logging/logger.go b/internal/logging/logger.go index 39156ec..3a8274c 100644 --- a/internal/logging/logger.go +++ b/internal/logging/logger.go @@ -50,7 +50,7 @@ var ( TimeFormat: time.RFC3339, PartsOrder: []string{"time", "level", "scope", "component", "message"}, FieldsExclude: []string{"scope", "component"}, - FormatPartValueByName: func(value interface{}, name string) string { + FormatPartValueByName: func(value any, name string) string { val := fmt.Sprintf("%s", value) if name == "component" { if value == nil { @@ -121,8 +121,8 @@ func (l *Logger) updateLogLevel() { continue } - scopes := strings.Split(strings.ToLower(env), ",") - for _, scope := range scopes { + scopes := strings.SplitSeq(strings.ToLower(env), ",") + for scope := range scopes { l.scopeLevels[scope] = level } } diff --git a/internal/logging/pion.go b/internal/logging/pion.go index 453b8bc..2676caf 100644 --- a/internal/logging/pion.go +++ b/internal/logging/pion.go @@ -13,32 +13,32 @@ type pionLogger struct { func (c pionLogger) Trace(msg string) { 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...) } func (c pionLogger) Debug(msg string) { 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...) } func (c pionLogger) Info(msg string) { 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...) } func (c pionLogger) Warn(msg string) { 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...) } func (c pionLogger) Error(msg string) { 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...) } diff --git a/internal/logging/utils.go b/internal/logging/utils.go index e622d96..73ae37a 100644 --- a/internal/logging/utils.go +++ b/internal/logging/utils.go @@ -13,7 +13,7 @@ func GetDefaultLogger() *zerolog.Logger { 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 if l == nil { l = &defaultLogger diff --git a/internal/network/hostname.go b/internal/network/hostname.go index d75255c..09d3996 100644 --- a/internal/network/hostname.go +++ b/internal/network/hostname.go @@ -42,7 +42,7 @@ func updateEtcHosts(hostname string, fqdn string) error { hostLine := fmt.Sprintf("127.0.1.1\t%s %s", hostname, fqdn) 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") { hostLineExists = true line = hostLine diff --git a/internal/network/utils.go b/internal/network/utils.go index 6d64332..797fd72 100644 --- a/internal/network/utils.go +++ b/internal/network/utils.go @@ -13,7 +13,7 @@ func lifetimeToTime(lifetime int) *time.Time { return &t } -func IsSame(a, b interface{}) bool { +func IsSame(a, b any) bool { aJSON, err := json.Marshal(a) if err != nil { return false diff --git a/internal/udhcpc/parser.go b/internal/udhcpc/parser.go index 66c3ba2..d75857c 100644 --- a/internal/udhcpc/parser.go +++ b/internal/udhcpc/parser.go @@ -101,7 +101,7 @@ func (l *Lease) SetLeaseExpiry() (time.Time, error) { func UnmarshalDHCPCLease(lease *Lease, str string) error { // parse the lease file as a map data := make(map[string]string) - for _, line := range strings.Split(str, "\n") { + for line := range strings.SplitSeq(str, "\n") { line = strings.TrimSpace(line) // skip empty lines and comments if line == "" || strings.HasPrefix(line, "#") { @@ -165,7 +165,7 @@ func UnmarshalDHCPCLease(lease *Lease, str string) error { field.Set(reflect.ValueOf(ip)) case []net.IP: val := make([]net.IP, 0) - for _, ipStr := range strings.Fields(value) { + for ipStr := range strings.FieldsSeq(value) { ip := net.ParseIP(ipStr) if ip == nil { continue diff --git a/internal/udhcpc/udhcpc.go b/internal/udhcpc/udhcpc.go index 128ea66..7b4d6e4 100644 --- a/internal/udhcpc/udhcpc.go +++ b/internal/udhcpc/udhcpc.go @@ -52,7 +52,7 @@ func NewDHCPClient(options *DHCPClientOptions) *DHCPClient { } func (c *DHCPClient) getWatchPaths() []string { - watchPaths := make(map[string]interface{}) + watchPaths := make(map[string]any) watchPaths[filepath.Dir(c.leaseFile)] = nil if c.pidFile != "" { diff --git a/internal/usbgadget/hid_keyboard.go b/internal/usbgadget/hid_keyboard.go index 6ad3b6a..f4fbaa6 100644 --- a/internal/usbgadget/hid_keyboard.go +++ b/internal/usbgadget/hid_keyboard.go @@ -1,10 +1,10 @@ package usbgadget import ( + "bytes" "context" "fmt" "os" - "reflect" "time" ) @@ -61,6 +61,8 @@ var keyboardReportDesc = []byte{ const ( hidReadBufferSize = 8 + hidKeyBufferSize = 6 + hidErrorRollOver = 0x01 // https://www.usb.org/sites/default/files/documents/hid1_11.pdf // https://www.usb.org/sites/default/files/hut1_2.pdf KeyboardLedMaskNumLock = 1 << 0 @@ -68,7 +70,9 @@ const ( KeyboardLedMaskScrollLock = 1 << 2 KeyboardLedMaskCompose = 1 << 3 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, @@ -81,6 +85,7 @@ type KeyboardState struct { ScrollLock bool `json:"scroll_lock"` Compose bool `json:"compose"` Kana bool `json:"kana"` + Shift bool `json:"shift"` // This is not part of the main USB HID spec } func getKeyboardState(b byte) KeyboardState { @@ -91,27 +96,27 @@ func getKeyboardState(b byte) KeyboardState { ScrollLock: b&KeyboardLedMaskScrollLock != 0, Compose: b&KeyboardLedMaskCompose != 0, Kana: b&KeyboardLedMaskKana != 0, + Shift: b&KeyboardLedMaskShift != 0, } } -func (u *UsbGadget) updateKeyboardState(b byte) { +func (u *UsbGadget) updateKeyboardState(state byte) { u.keyboardStateLock.Lock() defer u.keyboardStateLock.Unlock() - if b&^ValidKeyboardLedMasks != 0 { - u.log.Trace().Uint8("b", b).Msg("contains invalid bits, ignoring") + if state&^ValidKeyboardLedMasks != 0 { + u.log.Warn().Uint8("state", state).Msg("ignoring invalid bits") return } - newState := getKeyboardState(b) - if reflect.DeepEqual(u.keyboardState, newState) { + if u.keyboardState == state { return } - u.log.Info().Interface("old", u.keyboardState).Interface("new", newState).Msg("keyboardState updated") - u.keyboardState = newState + u.log.Trace().Uint8("old", u.keyboardState).Uint8("new", state).Msg("keyboardState updated") + u.keyboardState = state if u.onKeyboardStateChange != nil { - (*u.onKeyboardStateChange)(newState) + (*u.onKeyboardStateChange)(getKeyboardState(state)) } } @@ -123,7 +128,35 @@ func (u *UsbGadget) GetKeyboardState() KeyboardState { u.keyboardStateLock.Lock() 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() { @@ -142,7 +175,7 @@ func (u *UsbGadget) listenKeyboardEvents() { l.Info().Msg("context done") return default: - l.Trace().Msg("reading from keyboard") + l.Trace().Msg("reading from keyboard for LED state changes") if u.keyboardHidFile == nil { u.logWithSuppression("keyboardHidFileNil", 100, &l, nil, "keyboardHidFile is nil") // show the error every 100 times to avoid spamming the logs @@ -159,7 +192,7 @@ func (u *UsbGadget) listenKeyboardEvents() { } 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 { l.Trace().Int("n", n).Msg("expected 1 byte, got") continue @@ -195,12 +228,12 @@ func (u *UsbGadget) OpenKeyboardHidFile() error { 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 { return err } - _, err := u.keyboardHidFile.Write(data) + _, err := u.keyboardHidFile.Write(append([]byte{modifier, 0x00}, keys[:hidKeyBufferSize]...)) if err != nil { u.logWithSuppression("keyboardWriteHidFile", 100, u.log, err, "failed to write to hidg0") u.keyboardHidFile.Close() @@ -211,22 +244,145 @@ func (u *UsbGadget) keyboardWriteHidFile(data []byte) error { 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() defer u.keyboardLock.Unlock() + defer u.resetUserInputTime() - if len(keys) > 6 { - keys = keys[:6] + if len(keys) > hidKeyBufferSize { + keys = keys[:hidKeyBufferSize] } - if len(keys) < 6 { - keys = append(keys, make([]uint8, 6-len(keys))...) + if len(keys) < hidKeyBufferSize { + 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 { - return err + u.log.Warn().Uint8("modifier", modifier).Uints8("keys", keys).Msg("Could not write keyboard report to hidg0") } - u.resetUserInputTime() - return nil + return u.UpdateKeysDown(modifier, keys), err +} + +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 } diff --git a/internal/usbgadget/hid_mouse_absolute.go b/internal/usbgadget/hid_mouse_absolute.go index 2718f20..c083b60 100644 --- a/internal/usbgadget/hid_mouse_absolute.go +++ b/internal/usbgadget/hid_mouse_absolute.go @@ -85,17 +85,17 @@ func (u *UsbGadget) absMouseWriteHidFile(data []byte) error { 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() defer u.absMouseLock.Unlock() err := u.absMouseWriteHidFile([]byte{ - 1, // Report ID 1 - buttons, // Buttons - uint8(x), // X Low Byte - uint8(x >> 8), // X High Byte - uint8(y), // Y Low Byte - uint8(y >> 8), // Y High Byte + 1, // Report ID 1 + buttons, // Buttons + byte(x), // X Low Byte + byte(x >> 8), // X High Byte + byte(y), // Y Low Byte + byte(y >> 8), // Y High Byte }) if err != nil { return err diff --git a/internal/usbgadget/hid_mouse_relative.go b/internal/usbgadget/hid_mouse_relative.go index 786f265..70cb72c 100644 --- a/internal/usbgadget/hid_mouse_relative.go +++ b/internal/usbgadget/hid_mouse_relative.go @@ -75,15 +75,15 @@ func (u *UsbGadget) relMouseWriteHidFile(data []byte) error { 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() defer u.relMouseLock.Unlock() err := u.relMouseWriteHidFile([]byte{ - buttons, // Buttons - uint8(mx), // X - uint8(my), // Y - 0, // Wheel + buttons, // Buttons + byte(mx), // X + byte(my), // Y + 0, // Wheel }) if err != nil { return err diff --git a/internal/usbgadget/usbgadget.go b/internal/usbgadget/usbgadget.go index cb70655..3a01a44 100644 --- a/internal/usbgadget/usbgadget.go +++ b/internal/usbgadget/usbgadget.go @@ -41,6 +41,11 @@ var defaultUsbGadgetDevices = Devices{ MassStorage: true, } +type KeysDownState struct { + Modifier byte `json:"modifier"` + Keys ByteSlice `json:"keys"` +} + // UsbGadget is a struct that represents a USB gadget. type UsbGadget struct { name string @@ -60,7 +65,9 @@ type UsbGadget struct { relMouseHidFile *os.File 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 keyboardStateCtx context.Context keyboardStateCancel context.CancelFunc @@ -77,6 +84,7 @@ type UsbGadget struct { txLock sync.Mutex onKeyboardStateChange *func(state KeyboardState) + onKeysDownChange *func(state KeysDownState) log *zerolog.Logger @@ -122,7 +130,8 @@ func newUsbGadget(name string, configMap map[string]gadgetConfigItem, enabledDev txLock: sync.Mutex{}, keyboardStateCtx: keyboardCtx, 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, lastUserInput: time.Now(), log: logger, diff --git a/internal/usbgadget/utils.go b/internal/usbgadget/utils.go index 8654924..05fcd3a 100644 --- a/internal/usbgadget/utils.go +++ b/internal/usbgadget/utils.go @@ -2,6 +2,7 @@ package usbgadget import ( "bytes" + "encoding/json" "fmt" "path/filepath" "strconv" @@ -10,6 +11,31 @@ import ( "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 { pathArr := append([]string{basePath}, paths...) return filepath.Join(pathArr...) @@ -81,7 +107,7 @@ func compareFileContent(oldContent []byte, newContent []byte, looserMatch bool) 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() defer u.logSuppressionLock.Unlock() diff --git a/jsonrpc.go b/jsonrpc.go index 23766a5..6f9c670 100644 --- a/jsonrpc.go +++ b/jsonrpc.go @@ -13,29 +13,30 @@ import ( "time" "github.com/pion/webrtc/v4" + "github.com/rs/zerolog" "go.bug.st/serial" "github.com/jetkvm/kvm/internal/usbgadget" ) type JSONRPCRequest struct { - JSONRPC string `json:"jsonrpc"` - Method string `json:"method"` - Params map[string]interface{} `json:"params,omitempty"` - ID interface{} `json:"id,omitempty"` + JSONRPC string `json:"jsonrpc"` + Method string `json:"method"` + Params map[string]any `json:"params,omitempty"` + ID any `json:"id,omitempty"` } type JSONRPCResponse struct { - JSONRPC string `json:"jsonrpc"` - Result interface{} `json:"result,omitempty"` - Error interface{} `json:"error,omitempty"` - ID interface{} `json:"id"` + JSONRPC string `json:"jsonrpc"` + Result any `json:"result,omitempty"` + Error any `json:"error,omitempty"` + ID any `json:"id"` } type JSONRPCEvent struct { - JSONRPC string `json:"jsonrpc"` - Method string `json:"method"` - Params interface{} `json:"params,omitempty"` + JSONRPC string `json:"jsonrpc"` + Method string `json:"method"` + Params any `json:"params,omitempty"` } 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{ JSONRPC: "2.0", Method: event, @@ -102,7 +103,7 @@ func onRPCMessage(message webrtc.DataChannelMessage, session *Session) { errorResponse := JSONRPCResponse{ JSONRPC: "2.0", - Error: map[string]interface{}{ + Error: map[string]any{ "code": -32700, "message": "Parse error", }, @@ -123,7 +124,7 @@ func onRPCMessage(message webrtc.DataChannelMessage, session *Session) { if !ok { errorResponse := JSONRPCResponse{ JSONRPC: "2.0", - Error: map[string]interface{}{ + Error: map[string]any{ "code": -32601, "message": "Method not found", }, @@ -133,13 +134,12 @@ func onRPCMessage(message webrtc.DataChannelMessage, session *Session) { return } - scopedLogger.Trace().Msg("Calling RPC handler") - result, err := callRPCHandler(handler, request.Params) + result, err := callRPCHandler(scopedLogger, handler, request.Params) if err != nil { scopedLogger.Error().Err(err).Msg("Error calling RPC handler") errorResponse := JSONRPCResponse{ JSONRPC: "2.0", - Error: map[string]interface{}{ + Error: map[string]any{ "code": -32603, "message": "Internal error", "data": err.Error(), @@ -200,7 +200,7 @@ func rpcGetStreamQualityFactor() (float64, error) { func rpcSetStreamQualityFactor(factor float64) error { 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 { return err } @@ -240,7 +240,7 @@ func rpcSetEDID(edid string) error { } else { 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 { return err } @@ -467,12 +467,12 @@ func rpcSetTLSState(state TLSState) error { } type RPCHandler struct { - Func interface{} + Func any Params []string } // 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 defer func() { if r := recover(); r != nil { @@ -486,11 +486,11 @@ func callRPCHandler(handler RPCHandler, params map[string]interface{}) (result i }() // Call the handler - result, err = riskyCallRPCHandler(handler, params) - return result, err + result, err = riskyCallRPCHandler(logger, handler, params) + 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) handlerType := handlerValue.Type() @@ -499,20 +499,24 @@ func riskyCallRPCHandler(handler RPCHandler, params map[string]interface{}) (int } numParams := handlerType.NumIn() - args := make([]reflect.Value, numParams) - // Get the parameter names from the RPCHandler - paramNames := handler.Params + paramNames := handler.Params // Get the parameter names from the RPCHandler 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) paramName := paramNames[i] paramValue, ok := params[paramName] 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) @@ -529,7 +533,7 @@ func riskyCallRPCHandler(handler RPCHandler, params map[string]interface{}) (int if elemValue.Kind() == reflect.Float64 && paramType.Elem().Kind() == reflect.Uint8 { intValue := int(elemValue.Float()) 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)) } else { @@ -545,12 +549,12 @@ func riskyCallRPCHandler(handler RPCHandler, params map[string]interface{}) (int } else if paramType.Kind() == reflect.Struct && convertedValue.Kind() == reflect.Map { jsonData, err := json.Marshal(convertedValue.Interface()) 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() 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() } else { @@ -561,6 +565,7 @@ func riskyCallRPCHandler(handler RPCHandler, params map[string]interface{}) (int } } + logger.Trace().Msg("Calling RPC handler") results := handlerValue.Call(args) if len(results) == 0 { @@ -568,23 +573,32 @@ func riskyCallRPCHandler(handler RPCHandler, params map[string]interface{}) (int } if len(results) == 1 { - if results[0].Type().Implements(reflect.TypeOf((*error)(nil)).Elem()) { - if !results[0].IsNil() { - return nil, results[0].Interface().(error) + if ok, err := asError(results[0]); ok { + return nil, err + } + return results[0].Interface(), nil + } + + if len(results) == 2 { + if ok, err := asError(results[1]); ok { + if err != nil { + return nil, err } - return nil, nil } return results[0].Interface(), nil } - if len(results) == 2 && results[1].Type().Implements(reflect.TypeOf((*error)(nil)).Elem()) { - if !results[1].IsNil() { - return nil, results[1].Interface().(error) - } - return results[0].Interface(), nil - } + return nil, fmt.Errorf("too many return values from handler: %d", len(results)) +} - return nil, errors.New("unexpected return values from handler") +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) { @@ -923,7 +937,7 @@ func rpcSetKeyboardLayout(layout string) error { return nil } -func getKeyboardMacros() (interface{}, error) { +func getKeyboardMacros() (any, error) { macros := make([]KeyboardMacro, len(config.KeyboardMacros)) copy(macros, config.KeyboardMacros) @@ -931,10 +945,10 @@ func getKeyboardMacros() (interface{}, error) { } 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 { 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)) for i, item := range params.Macros { - macroMap, ok := item.(map[string]interface{}) + macroMap, ok := item.(map[string]any) if !ok { return nil, fmt.Errorf("invalid macro at index %d", i) } @@ -960,16 +974,16 @@ func setKeyboardMacros(params KeyboardMacrosParams) (interface{}, error) { } steps := []KeyboardMacroStep{} - if stepsArray, ok := macroMap["steps"].([]interface{}); ok { + if stepsArray, ok := macroMap["steps"].([]any); ok { for _, stepItem := range stepsArray { - stepMap, ok := stepItem.(map[string]interface{}) + stepMap, ok := stepItem.(map[string]any) if !ok { continue } step := KeyboardMacroStep{} - if keysArray, ok := stepMap["keys"].([]interface{}); ok { + if keysArray, ok := stepMap["keys"].([]any); ok { for _, k := range keysArray { if keyStr, ok := k.(string); ok { 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 { if modStr, ok := m.(string); ok { step.Modifiers = append(step.Modifiers, modStr) @@ -1047,6 +1061,8 @@ var rpcHandlers = map[string]RPCHandler{ "renewDHCPLease": {Func: rpcRenewDHCPLease}, "keyboardReport": {Func: rpcKeyboardReport, Params: []string{"modifier", "keys"}}, "getKeyboardLedState": {Func: rpcGetKeyboardLedState}, + "keypressReport": {Func: rpcKeypressReport, Params: []string{"key", "press"}}, + "getKeyDownState": {Func: rpcGetKeysDownState}, "absMouseReport": {Func: rpcAbsMouseReport, Params: []string{"x", "y", "buttons"}}, "relMouseReport": {Func: rpcRelMouseReport, Params: []string{"dx", "dy", "buttons"}}, "wheelReport": {Func: rpcWheelReport, Params: []string{"wheelY"}}, diff --git a/log.go b/log.go index b353a2c..1a091b1 100644 --- a/log.go +++ b/log.go @@ -5,7 +5,7 @@ import ( "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...) } diff --git a/native.go b/native.go index 9807206..67f423a 100644 --- a/native.go +++ b/native.go @@ -21,18 +21,18 @@ import ( var ctrlSocketConn net.Conn type CtrlAction struct { - Action string `json:"action"` - Seq int32 `json:"seq,omitempty"` - Params map[string]interface{} `json:"params,omitempty"` + Action string `json:"action"` + Seq int32 `json:"seq,omitempty"` + Params map[string]any `json:"params,omitempty"` } type CtrlResponse struct { - Seq int32 `json:"seq,omitempty"` - Error string `json:"error,omitempty"` - Errno int32 `json:"errno,omitempty"` - Result map[string]interface{} `json:"result,omitempty"` - Event string `json:"event,omitempty"` - Data json.RawMessage `json:"data,omitempty"` + Seq int32 `json:"seq,omitempty"` + Error string `json:"error,omitempty"` + Errno int32 `json:"errno,omitempty"` + Result map[string]any `json:"result,omitempty"` + Event string `json:"event,omitempty"` + Data json.RawMessage `json:"data,omitempty"` } type EventHandler func(event CtrlResponse) @@ -48,7 +48,7 @@ var ( 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() defer lock.Unlock() ctrlAction := CtrlAction{ @@ -429,7 +429,7 @@ func ensureBinaryUpdated(destPath string) error { func restoreHdmiEdid() { if config.EdidString != "" { 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 { nativeLogger.Warn().Err(err).Msg("Failed to restore HDMI EDID") } diff --git a/remote_mount.go b/remote_mount.go index befffcb..32a0fd2 100644 --- a/remote_mount.go +++ b/remote_mount.go @@ -27,10 +27,7 @@ func (w *WebRTCDiskReader) Read(ctx context.Context, offset int64, size int64) ( } mountedImageSize := currentVirtualMediaState.Size virtualMediaStateMutex.RUnlock() - end := offset + size - if end > mountedImageSize { - end = mountedImageSize - } + end := min(offset+size, mountedImageSize) req := DiskReadRequest{ Start: uint64(offset), End: uint64(end), diff --git a/ui/eslint.config.cjs b/ui/eslint.config.cjs index a6c0c1f..6e97258 100644 --- a/ui/eslint.config.cjs +++ b/ui/eslint.config.cjs @@ -66,6 +66,10 @@ module.exports = defineConfig([{ groups: ["builtin", "external", "internal", "parent", "sibling"], "newlines-between": "always", }], + + "@typescript-eslint/no-unused-vars": ["warn", { + "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" + }], }, settings: { diff --git a/ui/package-lock.json b/ui/package-lock.json index 72a4849..fbbf65e 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -1,12 +1,12 @@ { "name": "kvm-ui", - "version": "2025.08.07.001", + "version": "2025.08.15.2119", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "kvm-ui", - "version": "2025.08.07.001", + "version": "2025.08.15.2119", "dependencies": { "@headlessui/react": "^2.2.7", "@headlessui/tailwindcss": "^0.2.2", @@ -28,10 +28,10 @@ "react": "^19.1.1", "react-animate-height": "^3.2.3", "react-dom": "^19.1.1", - "react-hot-toast": "^2.5.2", + "react-hot-toast": "^2.6.0", "react-icons": "^5.5.0", "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-xtermjs": "^1.0.10", "recharts": "^2.15.3", @@ -41,22 +41,22 @@ "zustand": "^4.5.2" }, "devDependencies": { - "@eslint/compat": "^1.3.1", + "@eslint/compat": "^1.3.2", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "^9.32.0", + "@eslint/js": "^9.33.0", "@tailwindcss/forms": "^0.5.10", - "@tailwindcss/postcss": "^4.1.11", + "@tailwindcss/postcss": "^4.1.12", "@tailwindcss/typography": "^0.5.16", - "@tailwindcss/vite": "^4.1.11", - "@types/react": "^19.1.9", + "@tailwindcss/vite": "^4.1.12", + "@types/react": "^19.1.10", "@types/react-dom": "^19.1.7", "@types/semver": "^7.7.0", "@types/validator": "^13.15.2", - "@typescript-eslint/eslint-plugin": "^8.39.0", - "@typescript-eslint/parser": "^8.39.0", + "@typescript-eslint/eslint-plugin": "^8.39.1", + "@typescript-eslint/parser": "^8.39.1", "@vitejs/plugin-react-swc": "^3.10.2", "autoprefixer": "^10.4.21", - "eslint": "^9.32.0", + "eslint": "^9.33.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-import": "^2.32.0", "eslint-plugin-react": "^7.37.5", @@ -66,7 +66,7 @@ "postcss": "^8.5.6", "prettier": "^3.6.2", "prettier-plugin-tailwindcss": "^0.6.14", - "tailwindcss": "^4.1.11", + "tailwindcss": "^4.1.12", "typescript": "^5.9.2", "vite": "^6.3.5", "vite-tsconfig-paths": "^5.1.4" @@ -88,33 +88,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@babel/runtime": { - "version": "7.28.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.2.tgz", - "integrity": "sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.3.tgz", + "integrity": "sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz", - "integrity": "sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz", + "integrity": "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==", "cpu": [ "ppc64" ], @@ -128,9 +114,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.8.tgz", - "integrity": "sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.9.tgz", + "integrity": "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==", "cpu": [ "arm" ], @@ -144,9 +130,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.8.tgz", - "integrity": "sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz", + "integrity": "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==", "cpu": [ "arm64" ], @@ -160,9 +146,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.8.tgz", - "integrity": "sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.9.tgz", + "integrity": "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==", "cpu": [ "x64" ], @@ -176,9 +162,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.8.tgz", - "integrity": "sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz", + "integrity": "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==", "cpu": [ "arm64" ], @@ -192,9 +178,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.8.tgz", - "integrity": "sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz", + "integrity": "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==", "cpu": [ "x64" ], @@ -208,9 +194,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.8.tgz", - "integrity": "sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz", + "integrity": "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==", "cpu": [ "arm64" ], @@ -224,9 +210,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.8.tgz", - "integrity": "sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz", + "integrity": "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==", "cpu": [ "x64" ], @@ -240,9 +226,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.8.tgz", - "integrity": "sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz", + "integrity": "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==", "cpu": [ "arm" ], @@ -256,9 +242,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.8.tgz", - "integrity": "sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz", + "integrity": "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==", "cpu": [ "arm64" ], @@ -272,9 +258,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.8.tgz", - "integrity": "sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz", + "integrity": "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==", "cpu": [ "ia32" ], @@ -288,9 +274,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.8.tgz", - "integrity": "sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz", + "integrity": "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==", "cpu": [ "loong64" ], @@ -304,9 +290,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.8.tgz", - "integrity": "sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz", + "integrity": "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==", "cpu": [ "mips64el" ], @@ -320,9 +306,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.8.tgz", - "integrity": "sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz", + "integrity": "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==", "cpu": [ "ppc64" ], @@ -336,9 +322,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.8.tgz", - "integrity": "sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz", + "integrity": "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==", "cpu": [ "riscv64" ], @@ -352,9 +338,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.8.tgz", - "integrity": "sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz", + "integrity": "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==", "cpu": [ "s390x" ], @@ -368,9 +354,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.8.tgz", - "integrity": "sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz", + "integrity": "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==", "cpu": [ "x64" ], @@ -384,9 +370,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.8.tgz", - "integrity": "sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz", + "integrity": "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==", "cpu": [ "arm64" ], @@ -400,9 +386,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.8.tgz", - "integrity": "sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz", + "integrity": "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==", "cpu": [ "x64" ], @@ -416,9 +402,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.8.tgz", - "integrity": "sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz", + "integrity": "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==", "cpu": [ "arm64" ], @@ -432,9 +418,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.8.tgz", - "integrity": "sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz", + "integrity": "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==", "cpu": [ "x64" ], @@ -448,9 +434,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.8.tgz", - "integrity": "sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz", + "integrity": "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==", "cpu": [ "arm64" ], @@ -464,9 +450,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.8.tgz", - "integrity": "sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz", + "integrity": "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==", "cpu": [ "x64" ], @@ -480,9 +466,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.8.tgz", - "integrity": "sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz", + "integrity": "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==", "cpu": [ "arm64" ], @@ -496,9 +482,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.8.tgz", - "integrity": "sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz", + "integrity": "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==", "cpu": [ "ia32" ], @@ -512,9 +498,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.8.tgz", - "integrity": "sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz", + "integrity": "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==", "cpu": [ "x64" ], @@ -555,9 +541,9 @@ } }, "node_modules/@eslint/compat": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-1.3.1.tgz", - "integrity": "sha512-k8MHony59I5EPic6EQTCNOuPoVBnoYXkP+20xvwFjN7t0qI3ImyvyBgg+hIVPwC8JaxVjjUZld+cLfBLFDLucg==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-1.3.2.tgz", + "integrity": "sha512-jRNwzTbd6p2Rw4sZ1CgWRS8YMtqG15YyZf7zvb6gY2rB2u6n+2Z+ELW0GtL0fQgyl0pr4Y/BzBfng/BdsereRA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -587,18 +573,18 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", - "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz", + "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==", "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/core": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", - "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", + "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", "license": "Apache-2.0", "dependencies": { "@types/json-schema": "^7.0.15" @@ -643,9 +629,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.32.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.32.0.tgz", - "integrity": "sha512-BBpRFZK3eX6uMLKz8WxFOBIFFcGFJ/g8XuwjTHCqHROSIsopI+ddn/d5Cfh36+7+e5edVS8dbSHnBNhrLEX0zg==", + "version": "9.33.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.33.0.tgz", + "integrity": "sha512-5K1/mKhWaMfreBGJTwval43JJmkip0RmM+3+IuqupeSKNC/Th2Kc7ucaq5ovTSra/OOKB9c58CGSz3QMVbWt0A==", "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -664,12 +650,12 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz", - "integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", + "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.15.1", + "@eslint/core": "^0.15.2", "levn": "^0.4.1" }, "engines": { @@ -845,9 +831,9 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.12", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", - "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "dev": true, "license": "MIT", "dependencies": { @@ -855,6 +841,17 @@ "@jridgewell/trace-mapping": "^0.3.24" } }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -866,16 +863,16 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", - "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.29", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", - "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", + "version": "0.3.30", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", + "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", "dev": true, "license": "MIT", "dependencies": { @@ -1549,25 +1546,25 @@ } }, "node_modules/@tailwindcss/node": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.11.tgz", - "integrity": "sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.12.tgz", + "integrity": "sha512-3hm9brwvQkZFe++SBt+oLjo4OLDtkvlE8q2WalaD/7QWaeM7KEJbAiY/LJZUaCs7Xa8aUu4xy3uoyX4q54UVdQ==", "dev": true, "license": "MIT", "dependencies": { - "@ampproject/remapping": "^2.3.0", - "enhanced-resolve": "^5.18.1", - "jiti": "^2.4.2", + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.5.1", "lightningcss": "1.30.1", "magic-string": "^0.30.17", "source-map-js": "^1.2.1", - "tailwindcss": "4.1.11" + "tailwindcss": "4.1.12" } }, "node_modules/@tailwindcss/oxide": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.11.tgz", - "integrity": "sha512-Q69XzrtAhuyfHo+5/HMgr1lAiPP/G40OMFAnws7xcFEYqcypZmdW8eGXaOUIeOl1dzPJBPENXgbjsOyhg2nkrg==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.12.tgz", + "integrity": "sha512-gM5EoKHW/ukmlEtphNwaGx45fGoEmP10v51t9unv55voWh6WrOL19hfuIdo2FjxIaZzw776/BUQg7Pck++cIVw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -1579,24 +1576,24 @@ "node": ">= 10" }, "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.1.11", - "@tailwindcss/oxide-darwin-arm64": "4.1.11", - "@tailwindcss/oxide-darwin-x64": "4.1.11", - "@tailwindcss/oxide-freebsd-x64": "4.1.11", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.11", - "@tailwindcss/oxide-linux-arm64-gnu": "4.1.11", - "@tailwindcss/oxide-linux-arm64-musl": "4.1.11", - "@tailwindcss/oxide-linux-x64-gnu": "4.1.11", - "@tailwindcss/oxide-linux-x64-musl": "4.1.11", - "@tailwindcss/oxide-wasm32-wasi": "4.1.11", - "@tailwindcss/oxide-win32-arm64-msvc": "4.1.11", - "@tailwindcss/oxide-win32-x64-msvc": "4.1.11" + "@tailwindcss/oxide-android-arm64": "4.1.12", + "@tailwindcss/oxide-darwin-arm64": "4.1.12", + "@tailwindcss/oxide-darwin-x64": "4.1.12", + "@tailwindcss/oxide-freebsd-x64": "4.1.12", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.12", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.12", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.12", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.12", + "@tailwindcss/oxide-linux-x64-musl": "4.1.12", + "@tailwindcss/oxide-wasm32-wasi": "4.1.12", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.12", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.12" } }, "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.11.tgz", - "integrity": "sha512-3IfFuATVRUMZZprEIx9OGDjG3Ou3jG4xQzNTvjDoKmU9JdmoCohQJ83MYd0GPnQIu89YoJqvMM0G3uqLRFtetg==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.12.tgz", + "integrity": "sha512-oNY5pq+1gc4T6QVTsZKwZaGpBb2N1H1fsc1GD4o7yinFySqIuRZ2E4NvGasWc6PhYJwGK2+5YT1f9Tp80zUQZQ==", "cpu": [ "arm64" ], @@ -1611,9 +1608,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.11.tgz", - "integrity": "sha512-ESgStEOEsyg8J5YcMb1xl8WFOXfeBmrhAwGsFxxB2CxY9evy63+AtpbDLAyRkJnxLy2WsD1qF13E97uQyP1lfQ==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.12.tgz", + "integrity": "sha512-cq1qmq2HEtDV9HvZlTtrj671mCdGB93bVY6J29mwCyaMYCP/JaUBXxrQQQm7Qn33AXXASPUb2HFZlWiiHWFytw==", "cpu": [ "arm64" ], @@ -1628,9 +1625,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.11.tgz", - "integrity": "sha512-EgnK8kRchgmgzG6jE10UQNaH9Mwi2n+yw1jWmof9Vyg2lpKNX2ioe7CJdf9M5f8V9uaQxInenZkOxnTVL3fhAw==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.12.tgz", + "integrity": "sha512-6UCsIeFUcBfpangqlXay9Ffty9XhFH1QuUFn0WV83W8lGdX8cD5/+2ONLluALJD5+yJ7k8mVtwy3zMZmzEfbLg==", "cpu": [ "x64" ], @@ -1645,9 +1642,9 @@ } }, "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.11.tgz", - "integrity": "sha512-xdqKtbpHs7pQhIKmqVpxStnY1skuNh4CtbcyOHeX1YBE0hArj2romsFGb6yUmzkq/6M24nkxDqU8GYrKrz+UcA==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.12.tgz", + "integrity": "sha512-JOH/f7j6+nYXIrHobRYCtoArJdMJh5zy5lr0FV0Qu47MID/vqJAY3r/OElPzx1C/wdT1uS7cPq+xdYYelny1ww==", "cpu": [ "x64" ], @@ -1662,9 +1659,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.11.tgz", - "integrity": "sha512-ryHQK2eyDYYMwB5wZL46uoxz2zzDZsFBwfjssgB7pzytAeCCa6glsiJGjhTEddq/4OsIjsLNMAiMlHNYnkEEeg==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.12.tgz", + "integrity": "sha512-v4Ghvi9AU1SYgGr3/j38PD8PEe6bRfTnNSUE3YCMIRrrNigCFtHZ2TCm8142X8fcSqHBZBceDx+JlFJEfNg5zQ==", "cpu": [ "arm" ], @@ -1679,9 +1676,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.11.tgz", - "integrity": "sha512-mYwqheq4BXF83j/w75ewkPJmPZIqqP1nhoghS9D57CLjsh3Nfq0m4ftTotRYtGnZd3eCztgbSPJ9QhfC91gDZQ==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.12.tgz", + "integrity": "sha512-YP5s1LmetL9UsvVAKusHSyPlzSRqYyRB0f+Kl/xcYQSPLEw/BvGfxzbH+ihUciePDjiXwHh+p+qbSP3SlJw+6g==", "cpu": [ "arm64" ], @@ -1696,9 +1693,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.11.tgz", - "integrity": "sha512-m/NVRFNGlEHJrNVk3O6I9ggVuNjXHIPoD6bqay/pubtYC9QIdAMpS+cswZQPBLvVvEF6GtSNONbDkZrjWZXYNQ==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.12.tgz", + "integrity": "sha512-V8pAM3s8gsrXcCv6kCHSuwyb/gPsd863iT+v1PGXC4fSL/OJqsKhfK//v8P+w9ThKIoqNbEnsZqNy+WDnwQqCA==", "cpu": [ "arm64" ], @@ -1713,9 +1710,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.11.tgz", - "integrity": "sha512-YW6sblI7xukSD2TdbbaeQVDysIm/UPJtObHJHKxDEcW2exAtY47j52f8jZXkqE1krdnkhCMGqP3dbniu1Te2Fg==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.12.tgz", + "integrity": "sha512-xYfqYLjvm2UQ3TZggTGrwxjYaLB62b1Wiysw/YE3Yqbh86sOMoTn0feF98PonP7LtjsWOWcXEbGqDL7zv0uW8Q==", "cpu": [ "x64" ], @@ -1730,9 +1727,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.11.tgz", - "integrity": "sha512-e3C/RRhGunWYNC3aSF7exsQkdXzQ/M+aYuZHKnw4U7KQwTJotnWsGOIVih0s2qQzmEzOFIJ3+xt7iq67K/p56Q==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.12.tgz", + "integrity": "sha512-ha0pHPamN+fWZY7GCzz5rKunlv9L5R8kdh+YNvP5awe3LtuXb5nRi/H27GeL2U+TdhDOptU7T6Is7mdwh5Ar3A==", "cpu": [ "x64" ], @@ -1747,9 +1744,9 @@ } }, "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.11.tgz", - "integrity": "sha512-Xo1+/GU0JEN/C/dvcammKHzeM6NqKovG+6921MR6oadee5XPBaKOumrJCXvopJ/Qb5TH7LX/UAywbqrP4lax0g==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.12.tgz", + "integrity": "sha512-4tSyu3dW+ktzdEpuk6g49KdEangu3eCYoqPhWNsZgUhyegEda3M9rG0/j1GV/JjVVsj+lG7jWAyrTlLzd/WEBg==", "bundleDependencies": [ "@napi-rs/wasm-runtime", "@emnapi/core", @@ -1765,11 +1762,11 @@ "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "^1.4.3", - "@emnapi/runtime": "^1.4.3", - "@emnapi/wasi-threads": "^1.0.2", - "@napi-rs/wasm-runtime": "^0.2.11", - "@tybys/wasm-util": "^0.9.0", + "@emnapi/core": "^1.4.5", + "@emnapi/runtime": "^1.4.5", + "@emnapi/wasi-threads": "^1.0.4", + "@napi-rs/wasm-runtime": "^0.2.12", + "@tybys/wasm-util": "^0.10.0", "tslib": "^2.8.0" }, "engines": { @@ -1777,9 +1774,9 @@ } }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.11.tgz", - "integrity": "sha512-UgKYx5PwEKrac3GPNPf6HVMNhUIGuUh4wlDFR2jYYdkX6pL/rn73zTq/4pzUm8fOjAn5L8zDeHp9iXmUGOXZ+w==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.12.tgz", + "integrity": "sha512-iGLyD/cVP724+FGtMWslhcFyg4xyYyM+5F4hGvKA7eifPkXHRAUDFaimu53fpNg9X8dfP75pXx/zFt/jlNF+lg==", "cpu": [ "arm64" ], @@ -1794,9 +1791,9 @@ } }, "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.11.tgz", - "integrity": "sha512-YfHoggn1j0LK7wR82TOucWc5LDCguHnoS879idHekmmiR7g9HUtMw9MI0NHatS28u/Xlkfi9w5RJWgz2Dl+5Qg==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.12.tgz", + "integrity": "sha512-NKIh5rzw6CpEodv/++r0hGLlfgT/gFN+5WNdZtvh6wpU2BpGNgdjvj6H2oFc8nCM839QM1YOhjpgbAONUb4IxA==", "cpu": [ "x64" ], @@ -1811,17 +1808,17 @@ } }, "node_modules/@tailwindcss/postcss": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.11.tgz", - "integrity": "sha512-q/EAIIpF6WpLhKEuQSEVMZNMIY8KhWoAemZ9eylNAih9jxMGAYPPWBn3I9QL/2jZ+e7OEz/tZkX5HwbBR4HohA==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.12.tgz", + "integrity": "sha512-5PpLYhCAwf9SJEeIsSmCDLgyVfdBhdBpzX1OJ87anT9IVR0Z9pjM0FNixCAUAHGnMBGB8K99SwAheXrT0Kh6QQ==", "dev": true, "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", - "@tailwindcss/node": "4.1.11", - "@tailwindcss/oxide": "4.1.11", + "@tailwindcss/node": "4.1.12", + "@tailwindcss/oxide": "4.1.12", "postcss": "^8.4.41", - "tailwindcss": "4.1.11" + "tailwindcss": "4.1.12" } }, "node_modules/@tailwindcss/typography": { @@ -1841,15 +1838,15 @@ } }, "node_modules/@tailwindcss/vite": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.11.tgz", - "integrity": "sha512-RHYhrR3hku0MJFRV+fN2gNbDNEh3dwKvY8XJvTxCSXeMOsCRSr+uKvDWQcbizrHgjML6ZmTE5OwMrl5wKcujCw==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.12.tgz", + "integrity": "sha512-4pt0AMFDx7gzIrAOIYgYP0KCBuKWqyW8ayrdiLEjoJTT4pKTjrzG/e4uzWtTLDziC+66R9wbUqZBccJalSE5vQ==", "dev": true, "license": "MIT", "dependencies": { - "@tailwindcss/node": "4.1.11", - "@tailwindcss/oxide": "4.1.11", - "tailwindcss": "4.1.11" + "@tailwindcss/node": "4.1.12", + "@tailwindcss/oxide": "4.1.12", + "tailwindcss": "4.1.12" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" @@ -1964,9 +1961,9 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "19.1.9", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.9.tgz", - "integrity": "sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA==", + "version": "19.1.10", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.10.tgz", + "integrity": "sha512-EhBeSYX0Y6ye8pNebpKrwFJq7BoQ8J5SO6NlvNwwHjSj6adXJViPQrKlsyPw7hLBLvckEMO1yxeGdR82YBBlDg==", "license": "MIT", "dependencies": { "csstype": "^3.0.2" @@ -1996,17 +1993,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.39.0.tgz", - "integrity": "sha512-bhEz6OZeUR+O/6yx9Jk6ohX6H9JSFTaiY0v9/PuKT3oGK0rn0jNplLmyFUGV+a9gfYnVNwGDwS/UkLIuXNb2Rw==", + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.39.1.tgz", + "integrity": "sha512-yYegZ5n3Yr6eOcqgj2nJH8cH/ZZgF+l0YIdKILSDjYFRjgYQMgv/lRjV5Z7Up04b9VYUondt8EPMqg7kTWgJ2g==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.39.0", - "@typescript-eslint/type-utils": "8.39.0", - "@typescript-eslint/utils": "8.39.0", - "@typescript-eslint/visitor-keys": "8.39.0", + "@typescript-eslint/scope-manager": "8.39.1", + "@typescript-eslint/type-utils": "8.39.1", + "@typescript-eslint/utils": "8.39.1", + "@typescript-eslint/visitor-keys": "8.39.1", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -2020,7 +2017,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.39.0", + "@typescript-eslint/parser": "^8.39.1", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } @@ -2036,16 +2033,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.39.0.tgz", - "integrity": "sha512-g3WpVQHngx0aLXn6kfIYCZxM6rRJlWzEkVpqEFLT3SgEDsp9cpCbxxgwnE504q4H+ruSDh/VGS6nqZIDynP+vg==", + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.39.1.tgz", + "integrity": "sha512-pUXGCuHnnKw6PyYq93lLRiZm3vjuslIy7tus1lIQTYVK9bL8XBgJnCWm8a0KcTtHC84Yya1Q6rtll+duSMj0dg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.39.0", - "@typescript-eslint/types": "8.39.0", - "@typescript-eslint/typescript-estree": "8.39.0", - "@typescript-eslint/visitor-keys": "8.39.0", + "@typescript-eslint/scope-manager": "8.39.1", + "@typescript-eslint/types": "8.39.1", + "@typescript-eslint/typescript-estree": "8.39.1", + "@typescript-eslint/visitor-keys": "8.39.1", "debug": "^4.3.4" }, "engines": { @@ -2061,14 +2058,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.39.0.tgz", - "integrity": "sha512-CTzJqaSq30V/Z2Og9jogzZt8lJRR5TKlAdXmWgdu4hgcC9Kww5flQ+xFvMxIBWVNdxJO7OifgdOK4PokMIWPew==", + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.39.1.tgz", + "integrity": "sha512-8fZxek3ONTwBu9ptw5nCKqZOSkXshZB7uAxuFF0J/wTMkKydjXCzqqga7MlFMpHi9DoG4BadhmTkITBcg8Aybw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.39.0", - "@typescript-eslint/types": "^8.39.0", + "@typescript-eslint/tsconfig-utils": "^8.39.1", + "@typescript-eslint/types": "^8.39.1", "debug": "^4.3.4" }, "engines": { @@ -2083,14 +2080,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.39.0.tgz", - "integrity": "sha512-8QOzff9UKxOh6npZQ/4FQu4mjdOCGSdO3p44ww0hk8Vu+IGbg0tB/H1LcTARRDzGCC8pDGbh2rissBuuoPgH8A==", + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.39.1.tgz", + "integrity": "sha512-RkBKGBrjgskFGWuyUGz/EtD8AF/GW49S21J8dvMzpJitOF1slLEbbHnNEtAHtnDAnx8qDEdRrULRnWVx27wGBw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.39.0", - "@typescript-eslint/visitor-keys": "8.39.0" + "@typescript-eslint/types": "8.39.1", + "@typescript-eslint/visitor-keys": "8.39.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2101,9 +2098,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.39.0.tgz", - "integrity": "sha512-Fd3/QjmFV2sKmvv3Mrj8r6N8CryYiCS8Wdb/6/rgOXAWGcFuc+VkQuG28uk/4kVNVZBQuuDHEDUpo/pQ32zsIQ==", + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.39.1.tgz", + "integrity": "sha512-ePUPGVtTMR8XMU2Hee8kD0Pu4NDE1CN9Q1sxGSGd/mbOtGZDM7pnhXNJnzW63zk/q+Z54zVzj44HtwXln5CvHA==", "dev": true, "license": "MIT", "engines": { @@ -2118,15 +2115,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.39.0.tgz", - "integrity": "sha512-6B3z0c1DXVT2vYA9+z9axjtc09rqKUPRmijD5m9iv8iQpHBRYRMBcgxSiKTZKm6FwWw1/cI4v6em35OsKCiN5Q==", + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.39.1.tgz", + "integrity": "sha512-gu9/ahyatyAdQbKeHnhT4R+y3YLtqqHyvkfDxaBYk97EcbfChSJXyaJnIL3ygUv7OuZatePHmQvuH5ru0lnVeA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.39.0", - "@typescript-eslint/typescript-estree": "8.39.0", - "@typescript-eslint/utils": "8.39.0", + "@typescript-eslint/types": "8.39.1", + "@typescript-eslint/typescript-estree": "8.39.1", + "@typescript-eslint/utils": "8.39.1", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -2143,9 +2140,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.39.0.tgz", - "integrity": "sha512-ArDdaOllnCj3yn/lzKn9s0pBQYmmyme/v1HbGIGB0GB/knFI3fWMHloC+oYTJW46tVbYnGKTMDK4ah1sC2v0Kg==", + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.39.1.tgz", + "integrity": "sha512-7sPDKQQp+S11laqTrhHqeAbsCfMkwJMrV7oTDvtDds4mEofJYir414bYKUEb8YPUm9QL3U+8f6L6YExSoAGdQw==", "dev": true, "license": "MIT", "engines": { @@ -2157,16 +2154,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.39.0.tgz", - "integrity": "sha512-ndWdiflRMvfIgQRpckQQLiB5qAKQ7w++V4LlCHwp62eym1HLB/kw7D9f2e8ytONls/jt89TEasgvb+VwnRprsw==", + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.39.1.tgz", + "integrity": "sha512-EKkpcPuIux48dddVDXyQBlKdeTPMmALqBUbEk38McWv0qVEZwOpVJBi7ugK5qVNgeuYjGNQxrrnoM/5+TI/BPw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.39.0", - "@typescript-eslint/tsconfig-utils": "8.39.0", - "@typescript-eslint/types": "8.39.0", - "@typescript-eslint/visitor-keys": "8.39.0", + "@typescript-eslint/project-service": "8.39.1", + "@typescript-eslint/tsconfig-utils": "8.39.1", + "@typescript-eslint/types": "8.39.1", + "@typescript-eslint/visitor-keys": "8.39.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -2212,16 +2209,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.39.0.tgz", - "integrity": "sha512-4GVSvNA0Vx1Ktwvf4sFE+exxJ3QGUorQG1/A5mRfRNZtkBT2xrA/BCO2H0eALx/PnvCS6/vmYwRdDA41EoffkQ==", + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.39.1.tgz", + "integrity": "sha512-VF5tZ2XnUSTuiqZFXCZfZs1cgkdd3O/sSYmdo2EpSyDlC86UM/8YytTmKnehOW3TGAlivqTDT6bS87B/GQ/jyg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.39.0", - "@typescript-eslint/types": "8.39.0", - "@typescript-eslint/typescript-estree": "8.39.0" + "@typescript-eslint/scope-manager": "8.39.1", + "@typescript-eslint/types": "8.39.1", + "@typescript-eslint/typescript-estree": "8.39.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2236,13 +2233,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.39.0.tgz", - "integrity": "sha512-ldgiJ+VAhQCfIjeOgu8Kj5nSxds0ktPOSO9p4+0VDH2R2pLvQraaM5Oen2d7NxzMCm+Sn/vJT+mv2H5u6b/3fA==", + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.39.1.tgz", + "integrity": "sha512-W8FQi6kEh2e8zVhQ0eeRnxdvIoOkAp/CPAahcNio6nO9dsIwb9b34z90KOlheoyuVf6LSOEdjlkxSkapNEc+4A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.39.0", + "@typescript-eslint/types": "8.39.1", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -2650,9 +2647,9 @@ } }, "node_modules/browserslist": { - "version": "4.25.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", - "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", + "version": "4.25.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.2.tgz", + "integrity": "sha512-0si2SJK3ooGzIawRu61ZdPCO1IncZwS8IzuX73sPZsXW6EQ/w/DAfPyKI8l1ETTCr2MnvqWitmlCUxgdul45jA==", "dev": true, "funding": [ { @@ -2670,8 +2667,8 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001726", - "electron-to-chromium": "^1.5.173", + "caniuse-lite": "^1.0.30001733", + "electron-to-chromium": "^1.5.199", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, @@ -2739,9 +2736,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001731", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001731.tgz", - "integrity": "sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg==", + "version": "1.0.30001735", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001735.tgz", + "integrity": "sha512-EV/laoX7Wq2J9TQlyIXRxTJqIw4sxfXS4OYgudGxBYRuTv0q7AM6yMEpU/Vo1I94thg9U6EZ2NfZx9GJq83u7w==", "dev": true, "funding": [ { @@ -3159,9 +3156,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.198", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.198.tgz", - "integrity": "sha512-G5COfnp3w+ydVu80yprgWSfmfQaYRh9DOxfhAxstLyetKaLyl55QrNjx8C38Pc/C+RaDmb1M0Lk8wPEMQ+bGgQ==", + "version": "1.5.202", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.202.tgz", + "integrity": "sha512-NxbYjRmiHcHXV1Ws3fWUW+SLb62isauajk45LUJ/HgIOkUA7jLZu/X2Iif+X9FBNK8QkF9Zb4Q2mcwXCcY30mg==", "dev": true, "license": "ISC" }, @@ -3350,9 +3347,9 @@ } }, "node_modules/esbuild": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.8.tgz", - "integrity": "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz", + "integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==", "hasInstallScript": true, "license": "MIT", "bin": { @@ -3362,32 +3359,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.8", - "@esbuild/android-arm": "0.25.8", - "@esbuild/android-arm64": "0.25.8", - "@esbuild/android-x64": "0.25.8", - "@esbuild/darwin-arm64": "0.25.8", - "@esbuild/darwin-x64": "0.25.8", - "@esbuild/freebsd-arm64": "0.25.8", - "@esbuild/freebsd-x64": "0.25.8", - "@esbuild/linux-arm": "0.25.8", - "@esbuild/linux-arm64": "0.25.8", - "@esbuild/linux-ia32": "0.25.8", - "@esbuild/linux-loong64": "0.25.8", - "@esbuild/linux-mips64el": "0.25.8", - "@esbuild/linux-ppc64": "0.25.8", - "@esbuild/linux-riscv64": "0.25.8", - "@esbuild/linux-s390x": "0.25.8", - "@esbuild/linux-x64": "0.25.8", - "@esbuild/netbsd-arm64": "0.25.8", - "@esbuild/netbsd-x64": "0.25.8", - "@esbuild/openbsd-arm64": "0.25.8", - "@esbuild/openbsd-x64": "0.25.8", - "@esbuild/openharmony-arm64": "0.25.8", - "@esbuild/sunos-x64": "0.25.8", - "@esbuild/win32-arm64": "0.25.8", - "@esbuild/win32-ia32": "0.25.8", - "@esbuild/win32-x64": "0.25.8" + "@esbuild/aix-ppc64": "0.25.9", + "@esbuild/android-arm": "0.25.9", + "@esbuild/android-arm64": "0.25.9", + "@esbuild/android-x64": "0.25.9", + "@esbuild/darwin-arm64": "0.25.9", + "@esbuild/darwin-x64": "0.25.9", + "@esbuild/freebsd-arm64": "0.25.9", + "@esbuild/freebsd-x64": "0.25.9", + "@esbuild/linux-arm": "0.25.9", + "@esbuild/linux-arm64": "0.25.9", + "@esbuild/linux-ia32": "0.25.9", + "@esbuild/linux-loong64": "0.25.9", + "@esbuild/linux-mips64el": "0.25.9", + "@esbuild/linux-ppc64": "0.25.9", + "@esbuild/linux-riscv64": "0.25.9", + "@esbuild/linux-s390x": "0.25.9", + "@esbuild/linux-x64": "0.25.9", + "@esbuild/netbsd-arm64": "0.25.9", + "@esbuild/netbsd-x64": "0.25.9", + "@esbuild/openbsd-arm64": "0.25.9", + "@esbuild/openbsd-x64": "0.25.9", + "@esbuild/openharmony-arm64": "0.25.9", + "@esbuild/sunos-x64": "0.25.9", + "@esbuild/win32-arm64": "0.25.9", + "@esbuild/win32-ia32": "0.25.9", + "@esbuild/win32-x64": "0.25.9" } }, "node_modules/escalade": { @@ -3413,19 +3410,19 @@ } }, "node_modules/eslint": { - "version": "9.32.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.32.0.tgz", - "integrity": "sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg==", + "version": "9.33.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.33.0.tgz", + "integrity": "sha512-TS9bTNIryDzStCpJN93aC5VRSW3uTx9sClUn4B87pwiCaJh220otoI0X8mJKr+VcPtniMdN8GKjlwgWGUv5ZKA==", "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", - "@eslint/config-helpers": "^0.3.0", - "@eslint/core": "^0.15.0", + "@eslint/config-helpers": "^0.3.1", + "@eslint/core": "^0.15.2", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.32.0", - "@eslint/plugin-kit": "^0.3.4", + "@eslint/js": "9.33.0", + "@eslint/plugin-kit": "^0.3.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", @@ -4747,9 +4744,9 @@ } }, "node_modules/js-base64": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.7.tgz", - "integrity": "sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==", + "version": "3.7.8", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.8.tgz", + "integrity": "sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==", "license": "BSD-3-Clause" }, "node_modules/js-tokens": { @@ -5787,9 +5784,9 @@ } }, "node_modules/react-hot-toast": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.5.2.tgz", - "integrity": "sha512-Tun3BbCxzmXXM7C+NI4qiv6lT0uwGh4oAfeJyNOjYUejTsm35mK9iCaYLGv8cBz9L5YxZLx/2ii7zsIwPtPUdw==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.6.0.tgz", + "integrity": "sha512-bH+2EBMZ4sdyou/DPrfgIouFpcRLCJ+HoCA32UoAYHn6T3Ur5yfcDCeSr5mwldl6pFOsiocmrXMuoCJ1vV8bWg==", "license": "MIT", "dependencies": { "csstype": "^3.1.3", @@ -5851,9 +5848,9 @@ } }, "node_modules/react-simple-keyboard": { - "version": "3.8.106", - "resolved": "https://registry.npmjs.org/react-simple-keyboard/-/react-simple-keyboard-3.8.106.tgz", - "integrity": "sha512-ItCHCdhVCzn9huhenuyuHQMOGsl3UMLu5xAO1bkjj4AAgVoktFC1DQ4HWkOS6BGPvUJejFM3Q5hVM8Bl2oX9pA==", + "version": "3.8.111", + "resolved": "https://registry.npmjs.org/react-simple-keyboard/-/react-simple-keyboard-3.8.111.tgz", + "integrity": "sha512-zR6qeGeH1bhaP8GDMLwRBqpyU98jGUOmuNKZT6Z0056kjR4EVRo99Z/eVCafN0ySKpweQ6x0gVAjxkegy6EDFg==", "license": "MIT", "peerDependencies": { "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", @@ -6478,9 +6475,9 @@ } }, "node_modules/tailwindcss": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz", - "integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.12.tgz", + "integrity": "sha512-DzFtxOi+7NsFf7DBtI3BJsynR+0Yp6etH+nRPTbpWnS2pZBaSksv/JGctNwSWzbFjp0vxSqknaUylseZqMDGrA==", "license": "MIT" }, "node_modules/tapable": { @@ -6534,10 +6531,13 @@ } }, "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.4.6", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", - "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, "peerDependencies": { "picomatch": "^3 || ^4" }, @@ -6939,10 +6939,13 @@ } }, "node_modules/vite/node_modules/fdir": { - "version": "6.4.6", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", - "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, "peerDependencies": { "picomatch": "^3 || ^4" }, diff --git a/ui/package.json b/ui/package.json index 9f0c298..13cbbbd 100644 --- a/ui/package.json +++ b/ui/package.json @@ -1,7 +1,7 @@ { "name": "kvm-ui", "private": true, - "version": "2025.08.07.001", + "version": "2025.08.15.2119", "type": "module", "engines": { "node": "22.15.0" @@ -39,10 +39,10 @@ "react": "^19.1.1", "react-animate-height": "^3.2.3", "react-dom": "^19.1.1", - "react-hot-toast": "^2.5.2", + "react-hot-toast": "^2.6.0", "react-icons": "^5.5.0", "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-xtermjs": "^1.0.10", "recharts": "^2.15.3", @@ -52,22 +52,22 @@ "zustand": "^4.5.2" }, "devDependencies": { - "@eslint/compat": "^1.3.1", + "@eslint/compat": "^1.3.2", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "^9.32.0", + "@eslint/js": "^9.33.0", "@tailwindcss/forms": "^0.5.10", - "@tailwindcss/postcss": "^4.1.11", + "@tailwindcss/postcss": "^4.1.12", "@tailwindcss/typography": "^0.5.16", - "@tailwindcss/vite": "^4.1.11", - "@types/react": "^19.1.9", + "@tailwindcss/vite": "^4.1.12", + "@types/react": "^19.1.10", "@types/react-dom": "^19.1.7", "@types/semver": "^7.7.0", "@types/validator": "^13.15.2", - "@typescript-eslint/eslint-plugin": "^8.39.0", - "@typescript-eslint/parser": "^8.39.0", + "@typescript-eslint/eslint-plugin": "^8.39.1", + "@typescript-eslint/parser": "^8.39.1", "@vitejs/plugin-react-swc": "^3.10.2", "autoprefixer": "^10.4.21", - "eslint": "^9.32.0", + "eslint": "^9.33.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-import": "^2.32.0", "eslint-plugin-react": "^7.37.5", @@ -77,7 +77,7 @@ "postcss": "^8.5.6", "prettier": "^3.6.2", "prettier-plugin-tailwindcss": "^0.6.14", - "tailwindcss": "^4.1.11", + "tailwindcss": "^4.1.12", "typescript": "^5.9.2", "vite": "^6.3.5", "vite-tsconfig-paths": "^5.1.4" diff --git a/ui/src/components/ActionBar.tsx b/ui/src/components/ActionBar.tsx index 801cc7a..4f79d7e 100644 --- a/ui/src/components/ActionBar.tsx +++ b/ui/src/components/ActionBar.tsx @@ -26,17 +26,13 @@ export default function Actionbar({ requestFullscreen: () => Promise; }) { 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( 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 // 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; if (!open) { setTimeout(() => { - setDisableFocusTrap(false); - console.log("Popover is closing. Returning focus trap to video"); + setDisableVideoFocusTrap(false); + console.debug("Popover is closing. Returning focus trap to video"); }, 0); } } }, - [setDisableFocusTrap], + [setDisableVideoFocusTrap], ); return ( @@ -81,7 +77,7 @@ export default function Actionbar({ text="Paste text" LeadingIcon={MdOutlineContentPasteGo} onClick={() => { - setDisableFocusTrap(true); + setDisableVideoFocusTrap(true); }} /> @@ -123,7 +119,7 @@ export default function Actionbar({ ); }} onClick={() => { - setDisableFocusTrap(true); + setDisableVideoFocusTrap(true); }} /> @@ -154,7 +150,7 @@ export default function Actionbar({ theme="light" text="Wake on LAN" onClick={() => { - setDisableFocusTrap(true); + setDisableVideoFocusTrap(true); }} LeadingIcon={({ className }) => ( setVirtualKeyboard(!virtualKeyboard)} + onClick={() => setVirtualKeyboardEnabled(!isVirtualKeyboardEnabled)} /> @@ -218,7 +214,7 @@ export default function Actionbar({ text="Extension" LeadingIcon={LuCable} onClick={() => { - setDisableFocusTrap(true); + setDisableVideoFocusTrap(true); }} /> @@ -243,7 +239,7 @@ export default function Actionbar({ theme="light" text="Virtual Keyboard" LeadingIcon={FaKeyboard} - onClick={() => setVirtualKeyboard(!virtualKeyboard)} + onClick={() => setVirtualKeyboardEnabled(!isVirtualKeyboardEnabled)} />
@@ -268,7 +264,10 @@ export default function Actionbar({ theme="light" text="Settings" LeadingIcon={LuSettings} - onClick={() => navigateTo("/settings")} + onClick={() => { + setDisableVideoFocusTrap(true); + navigateTo("/settings") + }} />
diff --git a/ui/src/components/Header.tsx b/ui/src/components/Header.tsx index 543634a..4bb7a97 100644 --- a/ui/src/components/Header.tsx +++ b/ui/src/components/Header.tsx @@ -48,7 +48,7 @@ export default function DashboardNavbar({ navigate("/"); }, [navigate, setUser]); - const usbState = useHidStore(state => state.usbState); + const { usbState } = useHidStore(); // for testing //userEmail = "user@example.org"; diff --git a/ui/src/components/InfoBar.tsx b/ui/src/components/InfoBar.tsx index 7ce67a4..29f159d 100644 --- a/ui/src/components/InfoBar.tsx +++ b/ui/src/components/InfoBar.tsx @@ -1,4 +1,4 @@ -import { useEffect } from "react"; +import { useEffect, useMemo } from "react"; import { cx } from "@/cva.config"; import { @@ -7,65 +7,68 @@ import { useRTCStore, useSettingsStore, useVideoStore, + VideoState } from "@/hooks/stores"; import { keys, modifiers } from "@/keyboardMappings"; export default function InfoBar() { - const activeKeys = useHidStore(state => state.activeKeys); - const activeModifiers = useHidStore(state => state.activeModifiers); - const mouseX = useMouseStore(state => state.mouseX); - const mouseY = useMouseStore(state => state.mouseY); - const mouseMove = useMouseStore(state => state.mouseMove); + const { keysDownState } = useHidStore(); + const { mouseX, mouseY, mouseMove } = useMouseStore(); 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( - 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 settings = useSettingsStore(); - const showPressedKeys = useSettingsStore(state => state.showPressedKeys); + const { rpcDataChannel } = useRTCStore(); + const { debugMode, mouseMode, showPressedKeys } = useSettingsStore(); useEffect(() => { if (!rpcDataChannel) return; rpcDataChannel.onclose = () => console.log("rpcDataChannel has closed"); - rpcDataChannel.onerror = e => - console.log(`Error on DataChannel '${rpcDataChannel.label}': ${e}`); + rpcDataChannel.onerror = (e: Event) => + console.error(`Error on DataChannel '${rpcDataChannel.label}': ${e}`); }, [rpcDataChannel]); - const keyboardLedState = useHidStore(state => state.keyboardLedState); - const keyboardLedStateSyncAvailable = useHidStore(state => state.keyboardLedStateSyncAvailable); - const keyboardLedSync = useSettingsStore(state => state.keyboardLedSync); + const { keyboardLedState, usbState } = useHidStore(); + const { isTurnServerInUse } = useRTCStore(); + const { hdmiState } = useVideoStore(); - const isTurnServerInUse = useRTCStore(state => state.isTurnServerInUse); + const displayKeys = useMemo(() => { + if (!showPressedKeys) + return ""; - const usbState = useHidStore(state => state.usbState); - const hdmiState = useVideoStore(state => state.hdmiState); + const activeModifierMask = keysDownState.modifier || 0; + 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 (
- {settings.debugMode ? ( + {debugMode ? (
Resolution:{" "} {videoSize}
) : null} - {settings.debugMode ? ( + {debugMode ? (
Video Size: {videoClientSize}
) : null} - {(settings.debugMode && settings.mouseMode == "absolute") ? ( + {(debugMode && mouseMode == "absolute") ? (
Pointer: @@ -74,7 +77,7 @@ export default function InfoBar() {
) : null} - {(settings.debugMode && settings.mouseMode == "relative") ? ( + {(debugMode && mouseMode == "relative") ? (
Last Move: @@ -85,13 +88,13 @@ export default function InfoBar() {
) : null} - {settings.debugMode && ( + {debugMode && (
USB State: {usbState}
)} - {settings.debugMode && ( + {debugMode && (
HDMI State: {hdmiState} @@ -102,14 +105,7 @@ export default function InfoBar() {
Keys:

- {[ - ...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(", ")} + {displayKeys}

)} @@ -122,23 +118,10 @@ export default function InfoBar() {
)} - {keyboardLedStateSyncAvailable ? ( -
- {keyboardLedSync === "browser" ? "Browser" : "Host"} -
- ) : null}
Scroll Lock
- {keyboardLedState?.compose ? ( + {keyboardLedState.compose ? (
Compose
) : null} - {keyboardLedState?.kana ? ( + {keyboardLedState.kana ? (
Kana
) : null} + {keyboardLedState.shift ? ( +
+ Shift +
+ ) : null}
diff --git a/ui/src/components/JigglerSetting.tsx b/ui/src/components/JigglerSetting.tsx index d88b168..10fced1 100644 --- a/ui/src/components/JigglerSetting.tsx +++ b/ui/src/components/JigglerSetting.tsx @@ -30,7 +30,7 @@ export function JigglerSetting({ }, ); - const [send] = useJsonRpc(); + const { send } = useJsonRpc(); const [timezones, setTimezones] = useState([]); useEffect(() => { diff --git a/ui/src/components/MacroBar.tsx b/ui/src/components/MacroBar.tsx index 066c21f..0ba8cf4 100644 --- a/ui/src/components/MacroBar.tsx +++ b/ui/src/components/MacroBar.tsx @@ -10,7 +10,7 @@ import { useJsonRpc } from "@/hooks/useJsonRpc"; export default function MacroBar() { const { macros, initialized, loadMacros, setSendFn } = useMacrosStore(); const { executeMacro } = useKeyboard(); - const [send] = useJsonRpc(); + const { send } = useJsonRpc(); useEffect(() => { setSendFn(send); diff --git a/ui/src/components/MacroForm.tsx b/ui/src/components/MacroForm.tsx index f74c4ae..1aafe9c 100644 --- a/ui/src/components/MacroForm.tsx +++ b/ui/src/components/MacroForm.tsx @@ -1,17 +1,18 @@ import { useState } from "react"; import { LuPlus } from "react-icons/lu"; -import { KeySequence } from "@/hooks/stores"; import { Button } from "@/components/Button"; -import { InputFieldWithLabel, FieldError } from "@/components/InputField"; +import FieldLabel from "@/components/FieldLabel"; import Fieldset from "@/components/Fieldset"; +import { InputFieldWithLabel, FieldError } from "@/components/InputField"; import { MacroStepCard } from "@/components/MacroStepCard"; import { DEFAULT_DELAY, MAX_STEPS_PER_MACRO, MAX_KEYS_PER_STEP, } from "@/constants/macros"; -import FieldLabel from "@/components/FieldLabel"; +import { KeySequence } from "@/hooks/stores"; +import useKeyboardLayout from "@/hooks/useKeyboardLayout"; interface ValidationErrors { name?: string; @@ -44,6 +45,7 @@ export function MacroForm({ const [keyQueries, setKeyQueries] = useState>({}); const [errors, setErrors] = useState({}); const [errorMessage, setErrorMessage] = useState(null); + const { selectedKeyboard } = useKeyboardLayout(); const showTemporaryError = (message: string) => { setErrorMessage(message); @@ -234,6 +236,7 @@ export function MacroForm({ } onDelayChange={delay => handleDelayChange(stepIndex, delay)} isLastStep={stepIndex === (macro.steps?.length || 0) - 1} + keyboard={selectedKeyboard} /> ))}
diff --git a/ui/src/components/MacroStepCard.tsx b/ui/src/components/MacroStepCard.tsx index 8642c28..cf22468 100644 --- a/ui/src/components/MacroStepCard.tsx +++ b/ui/src/components/MacroStepCard.tsx @@ -1,23 +1,18 @@ +import { useMemo } from "react"; import { LuArrowUp, LuArrowDown, LuX, LuTrash2 } from "react-icons/lu"; import { Button } from "@/components/Button"; import { Combobox } from "@/components/Combobox"; import { SelectMenuBasic } from "@/components/SelectMenuBasic"; 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 { 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 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 => ({ value: modifier, label: modifier.replace(/^(Control|Alt|Shift|Meta)(Left|Right)$/, "$1 $2"), @@ -67,6 +62,7 @@ interface MacroStepCardProps { onModifierChange: (modifiers: string[]) => void; onDelayChange: (delay: number) => void; isLastStep: boolean; + keyboard: KeyboardLayout } const ensureArray = (arr: T[] | null | undefined): T[] => { @@ -84,9 +80,22 @@ export function MacroStepCard({ keyQuery, onModifierChange, onDelayChange, - isLastStep + isLastStep, + keyboard }: 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 availableKeys = keyOptions.filter(option => !selectedKeys.includes(option.value)); @@ -95,7 +104,7 @@ export function MacroStepCard({ } else { return availableKeys.filter(option => option.label.toLowerCase().includes(keyQuery.toLowerCase())); } - }; + }, [keyOptions, keyQuery, step.keys]); return ( @@ -204,7 +213,7 @@ export function MacroStepCard({ }} displayValue={() => keyQuery} onInputChange={onKeyQueryChange} - options={getFilteredKeys} + options={() => filteredKeys} disabledMessage="Max keys reached" size="SM" immediate diff --git a/ui/src/components/Terminal.tsx b/ui/src/components/Terminal.tsx index f5d662d..ba3e667 100644 --- a/ui/src/components/Terminal.tsx +++ b/ui/src/components/Terminal.tsx @@ -1,6 +1,6 @@ import "react-simple-keyboard/build/css/index.css"; import { ChevronDownIcon } from "@heroicons/react/16/solid"; -import { useEffect } from "react"; +import { useEffect, useMemo } from "react"; import { useXTerm } from "react-xtermjs"; import { FitAddon } from "@xterm/addon-fit"; import { WebLinksAddon } from "@xterm/addon-web-links"; @@ -65,21 +65,22 @@ function Terminal({ readonly dataChannel: RTCDataChannel; readonly type: AvailableTerminalTypes; }) { - const enableTerminal = useUiStore(state => state.terminalType == type); - const setTerminalType = useUiStore(state => state.setTerminalType); - const setDisableVideoFocusTrap = useUiStore(state => state.setDisableVideoFocusTrap); - + const { terminalType, setTerminalType, setDisableVideoFocusTrap } = useUiStore(); const { instance, ref } = useXTerm({ options: TERMINAL_CONFIG }); + const isTerminalTypeEnabled = useMemo(() => { + return terminalType == type; + }, [terminalType, type]); + useEffect(() => { setTimeout(() => { - setDisableVideoFocusTrap(enableTerminal); + setDisableVideoFocusTrap(isTerminalTypeEnabled); }, 500); return () => { setDisableVideoFocusTrap(false); }; - }, [enableTerminal, setDisableVideoFocusTrap]); + }, [setDisableVideoFocusTrap, isTerminalTypeEnabled]); const readyState = dataChannel.readyState; useEffect(() => { @@ -175,9 +176,9 @@ function Terminal({ ], { "pointer-events-none translate-y-[500px] opacity-100 transition duration-300": - !enableTerminal, + !isTerminalTypeEnabled, "pointer-events-auto -translate-y-[0px] opacity-100 transition duration-300": - enableTerminal, + isTerminalTypeEnabled, }, )} > diff --git a/ui/src/components/USBStateStatus.tsx b/ui/src/components/USBStateStatus.tsx index f0b2cb2..9321a19 100644 --- a/ui/src/components/USBStateStatus.tsx +++ b/ui/src/components/USBStateStatus.tsx @@ -4,9 +4,7 @@ import { cx } from "@/cva.config"; import KeyboardAndMouseConnectedIcon from "@/assets/keyboard-and-mouse-connected.png"; import LoadingSpinner from "@components/LoadingSpinner"; import StatusCard from "@components/StatusCards"; -import { HidState } from "@/hooks/stores"; - -type USBStates = HidState["usbState"]; +import { USBStates } from "@/hooks/stores"; type StatusProps = Record< USBStates, @@ -67,7 +65,7 @@ export default function USBStateStatus({ }; const props = StatusCardProps[state]; if (!props) { - console.log("Unsupported USB state: ", state); + console.warn("Unsupported USB state: ", state); return; } diff --git a/ui/src/components/UsbDeviceSetting.tsx b/ui/src/components/UsbDeviceSetting.tsx index 432ec3d..2a5193c 100644 --- a/ui/src/components/UsbDeviceSetting.tsx +++ b/ui/src/components/UsbDeviceSetting.tsx @@ -59,7 +59,7 @@ const usbPresets = [ ]; export function UsbDeviceSetting() { - const [send] = useJsonRpc(); + const { send } = useJsonRpc(); const [loading, setLoading] = useState(false); const [usbDeviceConfig, setUsbDeviceConfig] = diff --git a/ui/src/components/UsbInfoSetting.tsx b/ui/src/components/UsbInfoSetting.tsx index 198335c..dc6b474 100644 --- a/ui/src/components/UsbInfoSetting.tsx +++ b/ui/src/components/UsbInfoSetting.tsx @@ -54,7 +54,7 @@ const usbConfigs = [ type UsbConfigMap = Record; export function UsbInfoSetting() { - const [send] = useJsonRpc(); + const { send } = useJsonRpc(); const [loading, setLoading] = useState(false); const [usbConfigProduct, setUsbConfigProduct] = useState(""); @@ -101,8 +101,8 @@ export function UsbInfoSetting() { `Failed to load USB Config: ${resp.error.data || "Unknown error"}`, ); } else { - console.log("syncUsbConfigProduct#getUsbConfig result:", resp.result); const usbConfigState = resp.result as UsbConfigState; + console.log("syncUsbConfigProduct#getUsbConfig result:", usbConfigState); const product = usbConfigs.map(u => u.value).includes(usbConfigState.product) ? usbConfigState.product : "custom"; @@ -205,7 +205,7 @@ function USBConfigDialog({ product: "", }); - const [send] = useJsonRpc(); + const { send } = useJsonRpc(); const syncUsbConfig = useCallback(() => { send("getUsbConfig", {}, resp => { diff --git a/ui/src/components/VirtualKeyboard.tsx b/ui/src/components/VirtualKeyboard.tsx index 4ff04a9..876c7bc 100644 --- a/ui/src/components/VirtualKeyboard.tsx +++ b/ui/src/components/VirtualKeyboard.tsx @@ -1,4 +1,3 @@ -import { useShallow } from "zustand/react/shallow"; import { ChevronDownIcon } from "@heroicons/react/16/solid"; import { AnimatePresence, motion } from "framer-motion"; 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 DetachIconRaw from "@/assets/detach-icon.svg"; import { cx } from "@/cva.config"; -import { useHidStore, useSettingsStore, useUiStore } from "@/hooks/stores"; +import { useHidStore, useUiStore } from "@/hooks/stores"; 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 }) => { return Detach Icon; @@ -26,34 +26,47 @@ const AttachIcon = ({ className }: { className?: string }) => { }; function KeyboardWrapper() { - const [layoutName, setLayoutName] = useState("default"); - const keyboardRef = useRef(null); - const showAttachedVirtualKeyboard = useUiStore( - state => state.isAttachedVirtualKeyboardVisible, - ); - const setShowAttachedVirtualKeyboard = useUiStore( - state => state.setAttachedVirtualKeyboardVisibility, - ); - - const { sendKeyboardEvent, resetKeyboardState } = useKeyboard(); + const { isAttachedVirtualKeyboardVisible, setAttachedVirtualKeyboardVisibility } = useUiStore(); + const { keysDownState, /* keyboardLedState,*/ isVirtualKeyboardEnabled, setVirtualKeyboardEnabled } = useHidStore(); + const { handleKeyPress, executeMacro } = useKeyboard(); + const { selectedKeyboard } = useKeyboardLayout(); const [isDragging, setIsDragging] = useState(false); const [position, setPosition] = 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 keyboardLedStateSyncAvailable = useHidStore(state => state.keyboardLedStateSyncAvailable); - const keyboardLedSync = useSettingsStore(state => state.keyboardLedSync); - const isKeyboardLedManagedByHost = useMemo(() => - keyboardLedSync !== "browser" && keyboardLedStateSyncAvailable, - [keyboardLedSync, keyboardLedStateSyncAvailable], - ); + const virtualKeyboard = useMemo(() => { + return selectedKeyboard.virtualKeyboard; + }, [selectedKeyboard]); - 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) => { if (!keyboardRef.current) return; if (e instanceof TouchEvent && e.touches.length > 1) return; @@ -123,94 +136,69 @@ function KeyboardWrapper() { }; }, [endDrag, onDrag, startDrag]); + const onKeyUp = useCallback( + async (_: string, e: MouseEvent | undefined) => { + e?.preventDefault(); + e?.stopPropagation(); + }, + [] + ); + const onKeyDown = useCallback( - (key: string) => { - const isKeyShift = key === "{shift}" || key === "ShiftLeft" || key === "ShiftRight"; - const isKeyCaps = key === "CapsLock"; - 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")); - }; + async (key: string, e: MouseEvent | undefined) => { + e?.preventDefault(); + e?.stopPropagation(); + // handle the fake key-macros we have defined for common combinations if (key === "CtrlAltDelete") { - sendKeyboardEvent( - [keys["Delete"]], - [modifiers["ControlLeft"], modifiers["AltLeft"]], - ); - setTimeout(resetKeyboardState, 100); + await executeMacro([ { keys: ["Delete"], modifiers: ["ControlLeft", "AltLeft"], delay: 100 } ]); return; } if (key === "AltMetaEscape") { - sendKeyboardEvent( - [keys["Escape"]], - [modifiers["MetaLeft"], modifiers["AltLeft"]], - ); - - setTimeout(resetKeyboardState, 100); + await executeMacro([ { keys: ["Escape"], modifiers: ["AltLeft", "MetaLeft"], delay: 100 } ]); return; } if (key === "CtrlAltBackspace") { - sendKeyboardEvent( - [keys["Backspace"]], - [modifiers["ControlLeft"], modifiers["AltLeft"]], - ); - - setTimeout(resetKeyboardState, 100); + await executeMacro([ { keys: ["Backspace"], modifiers: ["ControlLeft", "AltLeft"], delay: 100 } ]); return; } - if (isKeyShift || isKeyCaps) { - toggleLayout(); - - if (isCapsLockActive) { - if (!isKeyboardLedManagedByHost) { - setIsCapsLockActive(false); - } - sendKeyboardEvent([keys["CapsLock"]], []); - return; - } + // if they press any of the latching keys, we send a keypress down event and the release it automatically (on timer) + if (latchingKeys.includes(key)) { + console.debug(`Latching key pressed: ${key} sending down and delayed up pair`); + handleKeyPress(keys[key], true) + setTimeout(() => handleKeyPress(keys[key], false), 100); + return; } - // Handle caps lock state change - if (isKeyCaps && !isKeyboardLedManagedByHost) { - setIsCapsLockActive(!isCapsLockActive); + // 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; } - // 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); + // otherwise, just treat it as a down+up pair + const cleanKey = key.replace(/[()]/g, ""); + console.debug(`Regular key pressed: ${cleanKey} sending down and up pair`); + handleKeyPress(keys[cleanKey], true); + setTimeout(() => handleKeyPress(keys[cleanKey], false), 50); }, - [isCapsLockActive, isKeyboardLedManagedByHost, sendKeyboardEvent, resetKeyboardState, setIsCapsLockActive], + [executeMacro, handleKeyPress, keyNamesForDownKeys], ); - const virtualKeyboard = useHidStore(state => state.isVirtualKeyboardEnabled); - const setVirtualKeyboard = useHidStore(state => state.setVirtualKeyboardEnabled); - return (
- {virtualKeyboard && ( + {isVirtualKeyboardEnabled && (
- {showAttachedVirtualKeyboard ? ( + {isAttachedVirtualKeyboardVisible ? (
@@ -266,7 +254,7 @@ function KeyboardWrapper() { theme="light" text="Hide" LeadingIcon={ChevronDownIcon} - onClick={() => setVirtualKeyboard(false)} + onClick={() => setVirtualKeyboardEnabled(false)} />
@@ -275,66 +263,73 @@ function KeyboardWrapper() {
+ { /* TODO add optional number pad */ }
diff --git a/ui/src/components/WebRTCVideo.tsx b/ui/src/components/WebRTCVideo.tsx index 4312c91..d4c7da4 100644 --- a/ui/src/components/WebRTCVideo.tsx +++ b/ui/src/components/WebRTCVideo.tsx @@ -9,9 +9,8 @@ import notifications from "@/notifications"; import useKeyboard from "@/hooks/useKeyboard"; import { useJsonRpc } from "@/hooks/useJsonRpc"; import { cx } from "@/cva.config"; -import { keys, modifiers } from "@/keyboardMappings"; +import { keys } from "@/keyboardMappings"; import { - useHidStore, useMouseStore, useRTCStore, useSettingsStore, @@ -28,15 +27,14 @@ import { export default function WebRTCVideo() { // Video and stream related refs and states const videoElm = useRef(null); - const mediaStream = useRTCStore(state => state.mediaStream); + const { mediaStream, peerConnectionState } = useRTCStore(); const [isPlaying, setIsPlaying] = useState(false); - const peerConnectionState = useRTCStore(state => state.peerConnectionState); const [isPointerLockActive, setIsPointerLockActive] = useState(false); + const [isKeyboardLockActive, setIsKeyboardLockActive] = useState(false); // Store hooks const settings = useSettingsStore(); - const { sendKeyboardEvent, resetKeyboardState } = useKeyboard(); - const setMousePosition = useMouseStore(state => state.setMousePosition); - const setMouseMove = useMouseStore(state => state.setMouseMove); + const { handleKeyPress, resetKeyboardState } = useKeyboard(); + const { setMousePosition, setMouseMove } = useMouseStore(); const { setClientSize: setVideoClientSize, setSize: setVideoSize, @@ -44,49 +42,39 @@ export default function WebRTCVideo() { height: videoHeight, clientWidth: videoClientWidth, clientHeight: videoClientHeight, + hdmiState, } = useVideoStore(); // Video enhancement settings - const videoSaturation = useSettingsStore(state => state.videoSaturation); - 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); + const { videoSaturation, videoBrightness, videoContrast } = useSettingsStore(); // RTC related states - const peerConnection = useRTCStore(state => state.peerConnection); + const { peerConnection } = useRTCStore(); // HDMI and UI states - const hdmiState = useVideoStore(state => state.hdmiState); const hdmiError = ["no_lock", "no_signal", "out_of_range"].includes(hdmiState); const isVideoLoading = !isPlaying; + // Mouse wheel states const [blockWheelEvent, setBlockWheelEvent] = useState(false); // Misc states and hooks - const [send] = useJsonRpc(); + const { send } = useJsonRpc(); // 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({ ref: videoElm as React.RefObject, - onResize: ({ width, height }) => { - // This is actually client size, not videoSize - if (width && height) { - if (!videoElm.current) return; - setVideoClientSize(width, height); - setVideoSize(videoElm.current.videoWidth, videoElm.current.videoHeight); - } - }, + onResize: handleResize, }); const updateVideoSizeStore = useCallback( @@ -107,15 +95,15 @@ export default function WebRTCVideo() { function updateVideoSizeOnMount() { if (videoElm.current) updateVideoSizeStore(videoElm.current); }, - [setVideoClientSize, updateVideoSizeStore, setVideoSize], + [updateVideoSizeStore], ); // Pointer lock and keyboard lock related const isPointerLockPossible = window.location.protocol === "https:" || window.location.hostname === "localhost"; const isFullscreenEnabled = document.fullscreenEnabled; - + 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 } @@ -149,29 +137,31 @@ export default function WebRTCVideo() { if (videoElm.current === null) return; const isKeyboardLockGranted = await checkNavigatorPermissions("keyboard-lock"); - - if (isKeyboardLockGranted && "keyboard" in navigator) { + + if (isKeyboardLockGranted && navigator && "keyboard" in navigator) { try { // @ts-expect-error - keyboard lock is not supported in all browsers - await navigator.keyboard.lock(); + await navigator.keyboard.lock(); + setIsKeyboardLockActive(true); } catch { // ignore errors } } - }, [checkNavigatorPermissions]); + }, [checkNavigatorPermissions, setIsKeyboardLockActive]); const releaseKeyboardLock = useCallback(async () => { if (videoElm.current === null || document.fullscreenElement !== videoElm.current) return; - if ("keyboard" in navigator) { - try { - // @ts-expect-error - keyboard unlock is not supported in all browsers - await navigator.keyboard.unlock(); - } catch { - // ignore errors - } + if (navigator && "keyboard" in navigator) { + try { + // @ts-expect-error - keyboard unlock is not supported in all browsers + await navigator.keyboard.unlock(); + } catch { + // ignore errors + } + setIsKeyboardLockActive(false); } - }, []); + }, [setIsKeyboardLockActive]); useEffect(() => { if (!isPointerLockPossible || !videoElm.current) return; @@ -197,7 +187,7 @@ export default function WebRTCVideo() { }, [isPointerLockPossible]); const requestFullscreen = useCallback(async () => { - if (!isFullscreenEnabled || !videoElm.current) return; + if (!isFullscreenEnabled || !videoElm.current) return; // per https://wicg.github.io/keyboard-lock/#system-key-press-handler // If keyboard lock is activated after fullscreen is already in effect, then the user my @@ -344,153 +334,58 @@ export default function WebRTCVideo() { sendAbsMouseMovement(0, 0, 0); }, [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( async (e: KeyboardEvent) => { e.preventDefault(); - const prev = useHidStore.getState(); - let code = e.code; - const key = e.key; + const code = getAdjustedKeyCode(e); + const hidKey = keys[code]; - if (!isKeyboardLedManagedByHost) { - setIsNumLockActive(e.getModifierState("NumLock")); - setIsCapsLockActive(e.getModifierState("CapsLock")); - setIsScrollLockActive(e.getModifierState("ScrollLock")); + if (hidKey === undefined) { + console.warn(`Key down not mapped: ${code}`); + return; } - 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 // event, so we need to clear the keys after a short delay // https://bugs.chromium.org/p/chromium/issues/detail?id=28089 // https://bugzilla.mozilla.org/show_bug.cgi?id=1299553 - if (e.metaKey) { + if (e.metaKey && hidKey < 0xE0) { setTimeout(() => { - const prev = useHidStore.getState(); - sendKeyboardEvent([], newModifiers || prev.activeModifiers); + console.debug(`Forcing the meta key release of associated key: ${hidKey}`); + handleKeyPress(hidKey, false); }, 10); } - - sendKeyboardEvent([...new Set(newKeys)], [...new Set(newModifiers)]); + console.debug(`Key down: ${hidKey}`); + handleKeyPress(hidKey, true); + + 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); + } }, - [ - handleModifierKeys, - sendKeyboardEvent, - isKeyboardLedManagedByHost, - setIsNumLockActive, - setIsCapsLockActive, - setIsScrollLockActive, - ], + [handleKeyPress, isKeyboardLockActive], ); const keyUpHandler = useCallback( - (e: KeyboardEvent) => { + async (e: KeyboardEvent) => { e.preventDefault(); - const prev = useHidStore.getState(); + const code = getAdjustedKeyCode(e); + const hidKey = keys[code]; - if (!isKeyboardLedManagedByHost) { - setIsNumLockActive(e.getModifierState("NumLock")); - setIsCapsLockActive(e.getModifierState("CapsLock")); - setIsScrollLockActive(e.getModifierState("ScrollLock")); + if (hidKey === undefined) { + console.warn(`Key up not mapped: ${code}`); + return; } - // Filtering out the key that was just released (keys[e.code]) - const newKeys = prev.activeKeys.filter(k => k !== keys[e.code]).filter(Boolean); - - // 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)]); + console.debug(`Key up: ${hidKey}`); + handleKeyPress(hidKey, false); }, - [ - handleModifierKeys, - sendKeyboardEvent, - isKeyboardLedManagedByHost, - setIsNumLockActive, - setIsCapsLockActive, - setIsScrollLockActive, - ], + [handleKeyPress], ); const videoKeyUpHandler = useCallback((e: KeyboardEvent) => { @@ -501,7 +396,7 @@ export default function WebRTCVideo() { // Fix only works in chrome based browsers. if (e.code === "Space") { if (videoElm.current.paused) { - console.log("Force playing video"); + console.debug("Force playing video"); videoElm.current.play(); } } @@ -544,13 +439,7 @@ export default function WebRTCVideo() { // We set the as early as possible addStreamToVideoElm(mediaStream); }, - [ - setVideoClientSize, - mediaStream, - updateVideoSizeStore, - peerConnection, - addStreamToVideoElm, - ], + [addStreamToVideoElm, mediaStream], ); // Setup Keyboard Events @@ -606,7 +495,7 @@ export default function WebRTCVideo() { videoElmRefValue.addEventListener("mousemove", 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, { signal, passive: true, @@ -663,10 +552,22 @@ export default function WebRTCVideo() { return isDefault ? {} // No filter if all settings are default (1.0) : { - filter: `saturate(${videoSaturation}) brightness(${videoBrightness}) contrast(${videoContrast})`, - }; + filter: `saturate(${videoSaturation}) brightness(${videoBrightness}) contrast(${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 (
@@ -699,48 +600,48 @@ export default function WebRTCVideo() {
-
diff --git a/ui/src/components/extensions/ATXPowerControl.tsx b/ui/src/components/extensions/ATXPowerControl.tsx index 0334a18..e276da1 100644 --- a/ui/src/components/extensions/ATXPowerControl.tsx +++ b/ui/src/components/extensions/ATXPowerControl.tsx @@ -23,7 +23,7 @@ export function ATXPowerControl() { > | null>(null); const [atxState, setAtxState] = useState(null); - const [send] = useJsonRpc(function onRequest(resp) { + const { send } = useJsonRpc(function onRequest(resp) { if (resp.method === "atxState") { setAtxState(resp.params as ATXState); } diff --git a/ui/src/components/extensions/DCPowerControl.tsx b/ui/src/components/extensions/DCPowerControl.tsx index a13e4ea..13bf128 100644 --- a/ui/src/components/extensions/DCPowerControl.tsx +++ b/ui/src/components/extensions/DCPowerControl.tsx @@ -19,7 +19,7 @@ interface DCPowerState { } export function DCPowerControl() { - const [send] = useJsonRpc(); + const { send } = useJsonRpc(); const [powerState, setPowerState] = useState(null); const getDCPowerState = useCallback(() => { diff --git a/ui/src/components/extensions/SerialConsole.tsx b/ui/src/components/extensions/SerialConsole.tsx index 544d3fd..d19f1f0 100644 --- a/ui/src/components/extensions/SerialConsole.tsx +++ b/ui/src/components/extensions/SerialConsole.tsx @@ -17,7 +17,7 @@ interface SerialSettings { } export function SerialConsole() { - const [send] = useJsonRpc(); + const { send } = useJsonRpc(); const [settings, setSettings] = useState({ baudRate: "9600", dataBits: "8", @@ -49,7 +49,7 @@ export function SerialConsole() { setSettings(newSettings); }); }; - const setTerminalType = useUiStore(state => state.setTerminalType); + const { setTerminalType } = useUiStore(); return (
diff --git a/ui/src/components/popovers/ExtensionPopover.tsx b/ui/src/components/popovers/ExtensionPopover.tsx index 10ee2ea..f6ec1f1 100644 --- a/ui/src/components/popovers/ExtensionPopover.tsx +++ b/ui/src/components/popovers/ExtensionPopover.tsx @@ -39,7 +39,7 @@ const AVAILABLE_EXTENSIONS: Extension[] = [ ]; export default function ExtensionPopover() { - const [send] = useJsonRpc(); + const { send } = useJsonRpc(); const [activeExtension, setActiveExtension] = useState(null); // Load active extension on component mount diff --git a/ui/src/components/popovers/MountPopover.tsx b/ui/src/components/popovers/MountPopover.tsx index 752398b..86ba623 100644 --- a/ui/src/components/popovers/MountPopover.tsx +++ b/ui/src/components/popovers/MountPopover.tsx @@ -21,8 +21,8 @@ import { useDeviceUiNavigation } from "@/hooks/useAppNavigation"; import notifications from "@/notifications"; const MountPopopover = forwardRef((_props, ref) => { - const diskDataChannelStats = useRTCStore(state => state.diskDataChannelStats); - const [send] = useJsonRpc(); + const { diskDataChannelStats } = useRTCStore(); + const { send } = useJsonRpc(); const { remoteVirtualMediaState, setModalView, setRemoteVirtualMediaState } = useMountMediaStore(); diff --git a/ui/src/components/popovers/PasteModal.tsx b/ui/src/components/popovers/PasteModal.tsx index 23a504a..1e4314b 100644 --- a/ui/src/components/popovers/PasteModal.tsx +++ b/ui/src/components/popovers/PasteModal.tsx @@ -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 { ExclamationCircleIcon } from "@heroicons/react/16/solid"; import { useClose } from "@headlessui/react"; @@ -10,7 +10,8 @@ import { SettingsPageHeader } from "@components/SettingsPageheader"; import { useJsonRpc } from "@/hooks/useJsonRpc"; import { useHidStore, useRTCStore, useUiStore, useSettingsStore } from "@/hooks/stores"; import { keys, modifiers } from "@/keyboardMappings"; -import { KeyStroke, KeyboardLayout, selectedKeyboard } from "@/keyboardLayouts"; +import { KeyStroke } from "@/keyboardLayouts"; +import useKeyboardLayout from "@/hooks/useKeyboardLayout"; import notifications from "@/notifications"; const hidKeyboardPayload = (modifier: number, keys: number[]) => { @@ -18,33 +19,24 @@ const hidKeyboardPayload = (modifier: number, keys: number[]) => { }; const modifierCode = (shift?: boolean, altRight?: boolean) => { - return (shift ? modifiers["ShiftLeft"] : 0) - | (altRight ? modifiers["AltRight"] : 0) + return (shift ? modifiers.ShiftLeft : 0) + | (altRight ? modifiers.AltRight : 0) } const noModifier = 0 export default function PasteModal() { const TextAreaRef = useRef(null); - const setPasteMode = useHidStore(state => state.setPasteModeEnabled); - const setDisableVideoFocusTrap = useUiStore(state => state.setDisableVideoFocusTrap); + const { setPasteModeEnabled } = useHidStore(); + const { setDisableVideoFocusTrap } = useUiStore(); - const [send] = useJsonRpc(); - const rpcDataChannel = useRTCStore(state => state.rpcDataChannel); + const { send } = useJsonRpc(); + const { rpcDataChannel } = useRTCStore(); const [invalidChars, setInvalidChars] = useState([]); const close = useClose(); - const keyboardLayout = useSettingsStore(state => state.keyboardLayout); - const setKeyboardLayout = useSettingsStore( - 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]); + const { setKeyboardLayout } = useSettingsStore(); + const { selectedKeyboard } = useKeyboardLayout(); useEffect(() => { send("getKeyboardLayout", {}, resp => { @@ -54,24 +46,23 @@ export default function PasteModal() { }, [send, setKeyboardLayout]); const onCancelPasteMode = useCallback(() => { - setPasteMode(false); + setPasteModeEnabled(false); setDisableVideoFocusTrap(false); setInvalidChars([]); - }, [setDisableVideoFocusTrap, setPasteMode]); + }, [setDisableVideoFocusTrap, setPasteModeEnabled]); const onConfirmPaste = useCallback(async () => { - setPasteMode(false); + setPasteModeEnabled(false); setDisableVideoFocusTrap(false); if (rpcDataChannel?.readyState !== "open" || !TextAreaRef.current) return; - const keyboard: KeyboardLayout = selectedKeyboard(safeKeyboardLayout); - if (!keyboard) return; + if (!selectedKeyboard) return; const text = TextAreaRef.current.value; try { for (const char of text) { - const keyprops = keyboard.chars[char]; + const keyprops = selectedKeyboard.chars[char]; if (!keyprops) continue; 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(() => { if (TextAreaRef.current) { @@ -161,7 +152,7 @@ export default function PasteModal() { // @ts-expect-error TS doesn't recognize Intl.Segmenter in some environments [...new Intl.Segmenter().segment(value)] .map(x => x.segment) - .filter(char => !selectedKeyboard(safeKeyboardLayout).chars[char]), + .filter(char => !selectedKeyboard.chars[char]), ), ]; @@ -182,7 +173,7 @@ export default function PasteModal() {

- Sending text using keyboard layout: {selectedKeyboard(safeKeyboardLayout).name} + Sending text using keyboard layout: {selectedKeyboard.isoCode}-{selectedKeyboard.name}

diff --git a/ui/src/components/popovers/WakeOnLan/Index.tsx b/ui/src/components/popovers/WakeOnLan/Index.tsx index 1cf7f18..e801052 100644 --- a/ui/src/components/popovers/WakeOnLan/Index.tsx +++ b/ui/src/components/popovers/WakeOnLan/Index.tsx @@ -14,11 +14,9 @@ import AddDeviceForm from "./AddDeviceForm"; export default function WakeOnLanModal() { const [storedDevices, setStoredDevices] = useState([]); const [showAddForm, setShowAddForm] = useState(false); - const setDisableVideoFocusTrap = useUiStore(state => state.setDisableVideoFocusTrap); - - const rpcDataChannel = useRTCStore(state => state.rpcDataChannel); - - const [send] = useJsonRpc(); + const { setDisableVideoFocusTrap } = useUiStore(); + const { rpcDataChannel } = useRTCStore(); + const { send } = useJsonRpc(); const close = useClose(); const [errorMessage, setErrorMessage] = useState(null); const [addDeviceErrorMessage, setAddDeviceErrorMessage] = useState(null); diff --git a/ui/src/components/sidebar/connectionStats.tsx b/ui/src/components/sidebar/connectionStats.tsx index 404deb1..3faf81b 100644 --- a/ui/src/components/sidebar/connectionStats.tsx +++ b/ui/src/components/sidebar/connectionStats.tsx @@ -37,10 +37,18 @@ function createChartArray( } export default function ConnectionStatsSidebar() { - const inboundRtpStats = useRTCStore(state => state.inboundRtpStats); - - const candidatePairStats = useRTCStore(state => state.candidatePairStats); - const setSidebarView = useUiStore(state => state.setSidebarView); + const { sidebarView, setSidebarView } = useUiStore(); + const { + mediaStream, + peerConnection, + inboundRtpStats, + appendInboundRtpStats, + candidatePairStats, + appendCandidatePairStats, + appendLocalCandidateStats, + appendRemoteCandidateStats, + appendDiskDataChannelStats, + } = useRTCStore(); function isMetricSupported( stream: Map, @@ -49,20 +57,6 @@ export default function ConnectionStatsSidebar() { 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() { (async () => { if (!mediaStream) return; @@ -80,8 +74,7 @@ export default function ConnectionStatsSidebar() { successfulLocalCandidateId = report.localCandidateId; successfulRemoteCandidateId = report.remoteCandidateId; } - - appendIceCandidatePair(report); + appendCandidatePairStats(report); } else if (report.type === "local-candidate") { // We only want to append the local candidate stats that were used in nominated candidate pair if (successfulLocalCandidateId === report.id) { diff --git a/ui/src/hooks/stores.ts b/ui/src/hooks/stores.ts index aa29528..f071825 100644 --- a/ui/src/hooks/stores.ts +++ b/ui/src/hooks/stores.ts @@ -47,12 +47,12 @@ export interface User { picture?: string; } -interface UserState { +export interface UserState { user: User | null; setUser: (user: User | null) => void; } -interface UIState { +export interface UIState { sidebarView: AvailableSidebarViews | null; setSidebarView: (view: AvailableSidebarViews | null) => void; @@ -68,21 +68,21 @@ interface UIState { setAttachedVirtualKeyboardVisibility: (enabled: boolean) => void; terminalType: AvailableTerminalTypes; - setTerminalType: (enabled: UIState["terminalType"]) => void; + setTerminalType: (type: UIState["terminalType"]) => void; } export const useUiStore = create(set => ({ terminalType: "none", - setTerminalType: type => set({ terminalType: type }), + setTerminalType: (type: UIState["terminalType"]) => set({ terminalType: type }), sidebarView: null, - setSidebarView: view => set({ sidebarView: view }), + setSidebarView: (view: AvailableSidebarViews | null) => set({ sidebarView: view }), disableVideoFocusTrap: false, - setDisableVideoFocusTrap: enabled => set({ disableVideoFocusTrap: enabled }), + setDisableVideoFocusTrap: (enabled: boolean) => set({ disableVideoFocusTrap: enabled }), isWakeOnLanModalVisible: false, - setWakeOnLanModalVisibility: enabled => set({ isWakeOnLanModalVisible: enabled }), + setWakeOnLanModalVisibility: (enabled: boolean) => set({ isWakeOnLanModalVisible: enabled }), toggleSidebarView: view => set(state => { @@ -94,11 +94,11 @@ export const useUiStore = create(set => ({ }), isAttachedVirtualKeyboardVisible: true, - setAttachedVirtualKeyboardVisibility: enabled => + setAttachedVirtualKeyboardVisibility: (enabled: boolean) => set({ isAttachedVirtualKeyboardVisible: enabled }), })); -interface RTCState { +export interface RTCState { peerConnection: RTCPeerConnection | null; setPeerConnection: (pc: RTCState["peerConnection"]) => void; @@ -118,18 +118,18 @@ interface RTCState { setMediaStream: (stream: MediaStream) => void; videoStreamStats: RTCInboundRtpStreamStats | null; - appendVideoStreamStats: (state: RTCInboundRtpStreamStats) => void; + appendVideoStreamStats: (stats: RTCInboundRtpStreamStats) => void; videoStreamStatsHistory: Map; isTurnServerInUse: boolean; setTurnServerInUse: (inUse: boolean) => void; inboundRtpStats: Map; - appendInboundRtpStats: (state: RTCInboundRtpStreamStats) => void; + appendInboundRtpStats: (stats: RTCInboundRtpStreamStats) => void; clearInboundRtpStats: () => void; candidatePairStats: Map; - appendCandidatePairStats: (pair: RTCIceCandidatePairStats) => void; + appendCandidatePairStats: (stats: RTCIceCandidatePairStats) => void; clearCandidatePairStats: () => void; // 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 diskDataChannelStats: Map; - appendDiskDataChannelStats: (stat: RTCDataChannelStats) => void; + appendDiskDataChannelStats: (stats: RTCDataChannelStats) => void; terminalChannel: RTCDataChannel | null; setTerminalChannel: (channel: RTCDataChannel) => void; @@ -149,78 +149,78 @@ interface RTCState { export const useRTCStore = create(set => ({ peerConnection: null, - setPeerConnection: pc => set({ peerConnection: pc }), + setPeerConnection: (pc: RTCState["peerConnection"]) => set({ peerConnection: pc }), rpcDataChannel: null, - setRpcDataChannel: channel => set({ rpcDataChannel: channel }), + setRpcDataChannel: (channel: RTCDataChannel) => set({ rpcDataChannel: channel }), transceiver: null, - setTransceiver: transceiver => set({ transceiver }), + setTransceiver: (transceiver: RTCRtpTransceiver) => set({ transceiver }), peerConnectionState: null, - setPeerConnectionState: state => set({ peerConnectionState: state }), + setPeerConnectionState: (state: RTCPeerConnectionState) => set({ peerConnectionState: state }), diskChannel: null, - setDiskChannel: channel => set({ diskChannel: channel }), + setDiskChannel: (channel: RTCDataChannel) => set({ diskChannel: channel }), mediaStream: null, - setMediaStream: stream => set({ mediaStream: stream }), + setMediaStream: (stream: MediaStream) => set({ mediaStream: stream }), videoStreamStats: null, - appendVideoStreamStats: stats => set({ videoStreamStats: stats }), + appendVideoStreamStats: (stats: RTCInboundRtpStreamStats) => set({ videoStreamStats: stats }), videoStreamStatsHistory: new Map(), isTurnServerInUse: false, - setTurnServerInUse: inUse => set({ isTurnServerInUse: inUse }), + setTurnServerInUse: (inUse: boolean) => set({ isTurnServerInUse: inUse }), inboundRtpStats: new Map(), - appendInboundRtpStats: newStat => { + appendInboundRtpStats: (stats: RTCInboundRtpStreamStats) => { set(prevState => ({ - inboundRtpStats: appendStatToMap(newStat, prevState.inboundRtpStats), + inboundRtpStats: appendStatToMap(stats, prevState.inboundRtpStats), })); }, clearInboundRtpStats: () => set({ inboundRtpStats: new Map() }), candidatePairStats: new Map(), - appendCandidatePairStats: newStat => { + appendCandidatePairStats: (stats: RTCIceCandidatePairStats) => { set(prevState => ({ - candidatePairStats: appendStatToMap(newStat, prevState.candidatePairStats), + candidatePairStats: appendStatToMap(stats, prevState.candidatePairStats), })); }, clearCandidatePairStats: () => set({ candidatePairStats: new Map() }), localCandidateStats: new Map(), - appendLocalCandidateStats: newStat => { + appendLocalCandidateStats: (stats: RTCIceCandidateStats) => { set(prevState => ({ - localCandidateStats: appendStatToMap(newStat, prevState.localCandidateStats), + localCandidateStats: appendStatToMap(stats, prevState.localCandidateStats), })); }, remoteCandidateStats: new Map(), - appendRemoteCandidateStats: newStat => { + appendRemoteCandidateStats: (stats: RTCIceCandidateStats) => { set(prevState => ({ - remoteCandidateStats: appendStatToMap(newStat, prevState.remoteCandidateStats), + remoteCandidateStats: appendStatToMap(stats, prevState.remoteCandidateStats), })); }, diskDataChannelStats: new Map(), - appendDiskDataChannelStats: newStat => { + appendDiskDataChannelStats: (stats: RTCDataChannelStats) => { set(prevState => ({ - diskDataChannelStats: appendStatToMap(newStat, prevState.diskDataChannelStats), + diskDataChannelStats: appendStatToMap(stats, prevState.diskDataChannelStats), })); }, // Add these new properties to the store implementation terminalChannel: null, - setTerminalChannel: channel => set({ terminalChannel: channel }), + setTerminalChannel: (channel: RTCDataChannel) => set({ terminalChannel: channel }), })); -interface MouseMove { +export interface MouseMove { x: number; y: number; buttons: number; } -interface MouseState { +export interface MouseState { mouseX: number; mouseY: number; mouseMove?: MouseMove; @@ -232,9 +232,17 @@ export const useMouseStore = create(set => ({ mouseX: 0, mouseY: 0, 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 + +export interface HdmiState { + ready: boolean; + error?: HdmiErrorStates; +} + export interface VideoState { width: number; height: number; @@ -242,19 +250,13 @@ export interface VideoState { clientHeight: number; setClientSize: (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: { ready: boolean; - error?: Extract; + error?: HdmiErrorStates; }) => void; } -export interface BacklightSettings { - max_brightness: number; - dim_after: number; - off_after: number; -} - export const useVideoStore = create(set => ({ width: 0, height: 0, @@ -263,13 +265,13 @@ export const useVideoStore = create(set => ({ clientHeight: 0, // The video element's client size - setClientSize: (clientWidth, clientHeight) => set({ clientWidth, clientHeight }), + setClientSize: (clientWidth: number, clientHeight: number) => set({ clientWidth, clientHeight }), // Resolution - setSize: (width, height) => set({ width, height }), + setSize: (width: number, height: number) => set({ width, height }), hdmiState: "connecting", - setHdmiState: state => { + setHdmiState: (state: HdmiState) => { if (!state) return; const { ready, error } = state; @@ -283,9 +285,13 @@ export const useVideoStore = create(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; setCursorVisibility: (enabled: boolean) => void; @@ -308,9 +314,6 @@ interface SettingsState { keyboardLayout: string; setKeyboardLayout: (layout: string) => void; - keyboardLedSync: KeyboardLedSync; - setKeyboardLedSync: (sync: KeyboardLedSync) => void; - scrollThrottling: number; setScrollThrottling: (value: number) => void; @@ -330,17 +333,17 @@ export const useSettingsStore = create( persist( set => ({ isCursorHidden: false, - setCursorVisibility: enabled => set({ isCursorHidden: enabled }), + setCursorVisibility: (enabled: boolean) => set({ isCursorHidden: enabled }), mouseMode: "absolute", - setMouseMode: mode => set({ mouseMode: mode }), + setMouseMode: (mode: string) => set({ mouseMode: mode }), debugMode: import.meta.env.DEV, - setDebugMode: enabled => set({ debugMode: enabled }), + setDebugMode: (enabled: boolean) => set({ debugMode: enabled }), // Add developer mode with default value developerMode: false, - setDeveloperMode: enabled => set({ developerMode: enabled }), + setDeveloperMode: (enabled: boolean) => set({ developerMode: enabled }), displayRotation: "270", setDisplayRotation: (rotation: string) => set({ displayRotation: rotation }), @@ -354,24 +357,21 @@ export const useSettingsStore = create( set({ backlightSettings: settings }), keyboardLayout: "en-US", - setKeyboardLayout: layout => set({ keyboardLayout: layout }), - - keyboardLedSync: "auto", - setKeyboardLedSync: sync => set({ keyboardLedSync: sync }), + setKeyboardLayout: (layout: string) => set({ keyboardLayout: layout }), scrollThrottling: 0, - setScrollThrottling: value => set({ scrollThrottling: value }), + setScrollThrottling: (value: number) => set({ scrollThrottling: value }), showPressedKeys: true, - setShowPressedKeys: show => set({ showPressedKeys: show }), + setShowPressedKeys: (show: boolean) => set({ showPressedKeys: show }), // Video enhancement settings with default values (1.0 = normal) videoSaturation: 1.0, - setVideoSaturation: value => set({ videoSaturation: value }), + setVideoSaturation: (value: number) => set({ videoSaturation: value }), videoBrightness: 1.0, - setVideoBrightness: value => set({ videoBrightness: value }), + setVideoBrightness: (value: number) => set({ videoBrightness: value }), videoContrast: 1.0, - setVideoContrast: value => set({ videoContrast: value }), + setVideoContrast: (value: number) => set({ videoContrast: value }), }), { name: "settings", @@ -411,23 +411,23 @@ export interface MountMediaState { export const useMountMediaStore = create(set => ({ localFile: null, - setLocalFile: file => set({ localFile: file }), + setLocalFile: (file: MountMediaState["localFile"]) => set({ localFile: file }), remoteVirtualMediaState: null, - setRemoteVirtualMediaState: state => set({ remoteVirtualMediaState: state }), + setRemoteVirtualMediaState: (state: MountMediaState["remoteVirtualMediaState"]) => set({ remoteVirtualMediaState: state }), modalView: "mode", - setModalView: view => set({ modalView: view }), + setModalView: (view: MountMediaState["modalView"]) => set({ modalView: view }), isMountMediaDialogOpen: false, - setIsMountMediaDialogOpen: isOpen => set({ isMountMediaDialogOpen: isOpen }), + setIsMountMediaDialogOpen: (isOpen: MountMediaState["isMountMediaDialogOpen"]) => set({ isMountMediaDialogOpen: isOpen }), uploadedFiles: [], - addUploadedFile: file => + addUploadedFile: (file: { name: string; size: string; uploadedAt: string }) => set(state => ({ uploadedFiles: [...state.uploadedFiles, file] })), errorMessage: null, - setErrorMessage: message => set({ errorMessage: message }), + setErrorMessage: (message: string | null) => set({ errorMessage: message }), })); export interface KeyboardLedState { @@ -436,41 +436,33 @@ export interface KeyboardLedState { scroll_lock: boolean; compose: boolean; kana: boolean; + shift: boolean; // Optional, as not all keyboards have a shift LED }; -const defaultKeyboardLedState: KeyboardLedState = { - num_lock: false, - caps_lock: false, - scroll_lock: false, - compose: false, - kana: false, -}; + +export const hidKeyBufferSize = 6; +export const hidErrorRollOver = 0x01; + +export interface KeysDownState { + modifier: number; + keys: number[]; +} + +export type USBStates = + | "configured" + | "attached" + | "not attached" + | "suspended" + | "addressed"; export interface HidState { - activeKeys: number[]; - 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; + keyboardLedState: KeyboardLedState; setKeyboardLedState: (state: KeyboardLedState) => void; - setIsNumLockActive: (active: boolean) => void; - setIsCapsLockActive: (active: boolean) => void; - setIsScrollLockActive: (active: boolean) => void; - keyboardLedStateSyncAvailable: boolean; - setKeyboardLedStateSyncAvailable: (available: boolean) => void; + keysDownState: KeysDownState; + setKeysDownState: (state: KeysDownState) => void; + + keyPressReportApiAvailable: boolean; + setkeyPressReportApiAvailable: (available: boolean) => void; isVirtualKeyboardEnabled: boolean; setVirtualKeyboardEnabled: (enabled: boolean) => void; @@ -478,55 +470,29 @@ export interface HidState { isPasteModeEnabled: boolean; setPasteModeEnabled: (enabled: boolean) => void; - usbState: "configured" | "attached" | "not attached" | "suspended" | "addressed"; - setUsbState: (state: HidState["usbState"]) => void; + usbState: USBStates; + setUsbState: (state: USBStates) => void; } -export const useHidStore = create((set, get) => ({ - activeKeys: [], - activeModifiers: [], - updateActiveKeysAndModifiers: ({ keys, modifiers }) => { - return set({ activeKeys: keys, activeModifiers: modifiers }); - }, +export const useHidStore = create(set => ({ + keyboardLedState: {} as KeyboardLedState, + setKeyboardLedState: (ledState: KeyboardLedState): void => set({ keyboardLedState: ledState }), - altGrArmed: false, - setAltGrArmed: armed => set({ altGrArmed: armed }), + keysDownState: { modifier: 0, keys: [0,0,0,0,0,0] } as KeysDownState, + setKeysDownState: (state: KeysDownState): void => set({ keysDownState: state }), - altGrTimer: 0, - setAltGrTimer: timeout => set({ altGrTimer: timeout }), - - 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 }), + keyPressReportApiAvailable: true, + setkeyPressReportApiAvailable: (available: boolean) => set({ keyPressReportApiAvailable: available }), isVirtualKeyboardEnabled: false, - setVirtualKeyboardEnabled: enabled => set({ isVirtualKeyboardEnabled: enabled }), + setVirtualKeyboardEnabled: (enabled: boolean): void => set({ isVirtualKeyboardEnabled: enabled }), isPasteModeEnabled: false, - setPasteModeEnabled: enabled => set({ isPasteModeEnabled: enabled }), + setPasteModeEnabled: (enabled: boolean): void => set({ isPasteModeEnabled: enabled }), // Add these new properties for USB state usbState: "not attached", - setUsbState: state => set({ usbState: state }), + setUsbState: (state: USBStates) => set({ usbState: state }), })); export const useUserStore = create(set => ({ @@ -534,11 +500,15 @@ export const useUserStore = create(set => ({ setUser: user => set({ user }), })); -export interface UpdateState { - isUpdatePending: boolean; - setIsUpdatePending: (isPending: boolean) => void; - updateDialogHasBeenMinimized: boolean; - otaState: { +export type UpdateModalViews = + | "loading" + | "updating" + | "upToDate" + | "updateAvailable" + | "updateCompleted" + | "error"; + +export interface OtaState { updating: boolean; error: string | null; @@ -567,24 +537,24 @@ export interface UpdateState { systemUpdateProgress: number; 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; - modalView: - | "loading" - | "updating" - | "upToDate" - | "updateAvailable" - | "updateCompleted" - | "error"; - setModalView: (view: UpdateState["modalView"]) => void; + modalView: UpdateModalViews + setModalView: (view: UpdateModalViews) => void; setUpdateErrorMessage: (errorMessage: string) => void; updateErrorMessage: string | null; } export const useUpdateStore = create(set => ({ isUpdatePending: false, - setIsUpdatePending: isPending => set({ isUpdatePending: isPending }), + setIsUpdatePending: (isPending: boolean) => set({ isUpdatePending: isPending }), setOtaState: state => set({ otaState: state }), otaState: { @@ -608,18 +578,22 @@ export const useUpdateStore = create(set => ({ }, updateDialogHasBeenMinimized: false, - setUpdateDialogHasBeenMinimized: hasBeenMinimized => + setUpdateDialogHasBeenMinimized: (hasBeenMinimized: boolean) => set({ updateDialogHasBeenMinimized: hasBeenMinimized }), modalView: "loading", - setModalView: view => set({ modalView: view }), + setModalView: (view: UpdateModalViews) => set({ modalView: view }), updateErrorMessage: null, - setUpdateErrorMessage: errorMessage => set({ updateErrorMessage: errorMessage }), + setUpdateErrorMessage: (errorMessage: string) => set({ updateErrorMessage: errorMessage }), })); -interface UsbConfigModalState { - modalView: "updateUsbConfig" | "updateUsbConfigSuccess"; +export type UsbConfigModalViews = + | "updateUsbConfig" + | "updateUsbConfigSuccess"; + +export interface UsbConfigModalState { + modalView: UsbConfigModalViews ; errorMessage: string | null; - setModalView: (view: UsbConfigModalState["modalView"]) => void; + setModalView: (view: UsbConfigModalViews) => void; setErrorMessage: (message: string | null) => void; } @@ -634,24 +608,26 @@ export interface UsbConfigState { export const useUsbConfigModalStore = create(set => ({ modalView: "updateUsbConfig", errorMessage: null, - setModalView: view => set({ modalView: view }), - setErrorMessage: message => set({ errorMessage: message }), + setModalView: (view: UsbConfigModalViews) => set({ modalView: view }), + setErrorMessage: (message: string | null) => set({ errorMessage: message }), })); -interface LocalAuthModalState { - modalView: - | "createPassword" - | "deletePassword" - | "updatePassword" - | "creationSuccess" - | "deleteSuccess" - | "updateSuccess"; - setModalView: (view: LocalAuthModalState["modalView"]) => void; +export type LocalAuthModalViews = + | "createPassword" + | "deletePassword" + | "updatePassword" + | "creationSuccess" + | "deleteSuccess" + | "updateSuccess"; + +export interface LocalAuthModalState { + modalView:LocalAuthModalViews; + setModalView: (view:LocalAuthModalViews) => void; } export const useLocalAuthModalStore = create(set => ({ modalView: "createPassword", - setModalView: view => set({ modalView: view }), + setModalView: (view: LocalAuthModalViews) => set({ modalView: view }), })); export interface DeviceState { @@ -666,8 +642,8 @@ export const useDeviceStore = create(set => ({ appVersion: null, systemVersion: null, - setAppVersion: version => set({ appVersion: version }), - setSystemVersion: version => set({ systemVersion: version }), + setAppVersion: (version: string) => set({ appVersion: version }), + setSystemVersion: (version: string) => set({ systemVersion: version }), })); export interface DhcpLease { @@ -833,7 +809,7 @@ export const useMacrosStore = create((set, get) => ({ try { await new Promise((resolve, reject) => { - sendFn("getKeyboardMacros", {}, response => { + sendFn("getKeyboardMacros", {}, (response: JsonRpcResponse) => { if (response.error) { console.error("Error loading macros:", response.error); reject(new Error(response.error.message)); @@ -913,7 +889,7 @@ export const useMacrosStore = create((set, get) => ({ sendFn( "setKeyboardMacros", { params: { macros: macrosWithSortOrder } }, - response => { + (response: JsonRpcResponse) => { resolve(response); }, ); diff --git a/ui/src/hooks/useJsonRpc.ts b/ui/src/hooks/useJsonRpc.ts index 92b56ff..b4fcc8e 100644 --- a/ui/src/hooks/useJsonRpc.ts +++ b/ui/src/hooks/useJsonRpc.ts @@ -33,10 +33,10 @@ const callbackStore = new Map void>( let requestCounter = 0; export function useJsonRpc(onRequest?: (payload: JsonRpcRequest) => void) { - const rpcDataChannel = useRTCStore(state => state.rpcDataChannel); + const { rpcDataChannel } = useRTCStore(); 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; 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], + [rpcDataChannel] ); useEffect(() => { @@ -61,7 +61,7 @@ export function useJsonRpc(onRequest?: (payload: JsonRpcRequest) => void) { return; } - if ("error" in payload) console.error(payload.error); + if ("error" in payload) console.error("RPC error", payload); if (!payload.id) return; const callback = callbackStore.get(payload.id); @@ -76,7 +76,8 @@ export function useJsonRpc(onRequest?: (payload: JsonRpcRequest) => void) { return () => { rpcDataChannel.removeEventListener("message", messageHandler); }; - }, [rpcDataChannel, onRequest]); + }, + [rpcDataChannel, onRequest]); - return [send]; + return { send }; } diff --git a/ui/src/hooks/useKeyboard.ts b/ui/src/hooks/useKeyboard.ts index 0ce1eef..5f587b0 100644 --- a/ui/src/hooks/useKeyboard.ts +++ b/ui/src/hooks/useKeyboard.ts @@ -1,42 +1,117 @@ import { useCallback } from "react"; -import { useHidStore, useRTCStore } from "@/hooks/stores"; -import { useJsonRpc } from "@/hooks/useJsonRpc"; -import { keys, modifiers } from "@/keyboardMappings"; +import { KeysDownState, useHidStore, useRTCStore, hidKeyBufferSize, hidErrorRollOver } from "@/hooks/stores"; +import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc"; +import { hidKeyToModifierMask, keys, modifiers } from "@/keyboardMappings"; export default function useKeyboard() { - const [send] = useJsonRpc(); + const { send } = useJsonRpc(); + const { rpcDataChannel } = useRTCStore(); + const { keysDownState, setKeysDownState } = useHidStore(); - const rpcDataChannel = useRTCStore(state => state.rpcDataChannel); - const updateActiveKeysAndModifiers = useHidStore( - state => state.updateActiveKeysAndModifiers, - ); + // INTRODUCTION: The earlier version of the JetKVM device shipped with all keyboard state + // being tracked on the browser/client-side. When adding the keyPressReport API to the + // 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( - (keys: number[], modifiers: number[]) => { + async (state: KeysDownState) => { 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 - updateActiveKeysAndModifiers({ keys: keys, modifiers: modifiers }); + if (keysDownState) { + 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(() => { - sendKeyboardEvent([], []); - }, [sendKeyboardEvent]); + // sendKeypressEvent is used to send a single key press/release event to the device. + // It sends the key and whether it is pressed or released. + // 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 }[]) => { for (const [index, step] of steps.entries()) { - const keyValues = step.keys?.map(key => keys[key]).filter(Boolean) || []; - const modifierValues = step.modifiers?.map(mod => modifiers[mod]).filter(Boolean) || []; + const keyValues = (step.keys || []).map(key => keys[key]).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 (keyValues.length > 0 || modifierValues.length > 0) { - sendKeyboardEvent(keyValues, modifierValues); + if (keyValues.length > 0 || modifierMask > 0) { + sendKeyboardEvent({ keys: keyValues, modifier: modifierMask }); await new Promise(resolve => setTimeout(resolve, step.delay || 50)); 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 }; } diff --git a/ui/src/hooks/useKeyboardLayout.ts b/ui/src/hooks/useKeyboardLayout.ts new file mode 100644 index 0000000..c1d0557 --- /dev/null +++ b/ui/src/hooks/useKeyboardLayout.ts @@ -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 }; +} \ No newline at end of file diff --git a/ui/src/index.css b/ui/src/index.css index 44acd2a..db03b42 100644 --- a/ui/src/index.css +++ b/ui/src/index.css @@ -315,6 +315,11 @@ video::-webkit-media-controls { @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:not(:last-child) { @apply mr-[2px]! md:mr-[5px]!; diff --git a/ui/src/keyboardLayouts.ts b/ui/src/keyboardLayouts.ts index 4ae3ad9..4ae8970 100644 --- a/ui/src/keyboardLayouts.ts +++ b/ui/src/keyboardLayouts.ts @@ -1,9 +1,20 @@ export interface KeyStroke { modifier: number; keys: number[]; } export interface KeyInfo { key: string | number; shift?: boolean, altRight?: boolean } export interface KeyCombo extends KeyInfo { deadKey?: boolean, accentKey?: KeyInfo } -export interface KeyboardLayout { isoCode: string, name: string, chars: Record } +export interface KeyboardLayout { + isoCode: string; + name: string; + chars: Record; + modifierDisplayMap: Record; + keyDisplayMap: Record; + 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 { de_CH } from "@/keyboardLayouts/de_CH" 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" 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 } - }); -} diff --git a/ui/src/keyboardLayouts/cs_CZ.ts b/ui/src/keyboardLayouts/cs_CZ.ts index e4f8822..c02be70 100644 --- a/ui/src/keyboardLayouts/cs_CZ.ts +++ b/ui/src/keyboardLayouts/cs_CZ.ts @@ -1,17 +1,20 @@ 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 keyAcute = { key: "Equal" } // accent aigu (acute accent), mark ´ placed above the letter -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 keyGrave = { key: "Digit7", shift: true, altRight: true } // accent grave, mark ` placed above the letter -const keyTilde = { key: "Digit1", shift: true, altRight: true } // tilde, mark ~ placed above the letter -const keyRing = { key: "Backquote", shift: true } // kroužek (little ring), mark ° placed above the letter -const keyOverdot = { key: "Digit8", shift: true, altRight: true } // overdot (dot above), mark ˙ placed above the letter -const keyHook = { key: "Digit6", shift: true, altRight: true } // ogonoek (little hook), mark ˛ placed beneath a letter -const keyCedille = { key: "Equal", shift: true, altRight: true } // accent cedille (cedilla), mark ¸ placed beneath a letter +const name = "Čeština"; +const isoCode = "cs-CZ"; + +const keyTrema: KeyCombo = { key: "Backslash" } // tréma (umlaut), two dots placed above a vowel +const keyAcute: KeyCombo = { key: "Equal" } // accent aigu (acute accent), mark ´ placed above the letter +const keyHat: KeyCombo = { key: "Digit3", shift: true, altRight: true } // accent circonflexe (accent hat), mark ^ placed above the letter +const keyCaron: KeyCombo = { key: "Equal", shift: true } // caron or haček (inverted hat), mark ˇ placed above the letter +const keyGrave: KeyCombo = { key: "Digit7", shift: true, altRight: true } // accent grave, mark ` placed above the letter +const keyTilde: KeyCombo = { key: "Digit1", shift: true, altRight: true } // tilde, mark ~ placed above the 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 = { A: { key: "KeyA", shift: true }, @@ -244,7 +247,11 @@ const chars = { } as Record; export const cs_CZ: KeyboardLayout = { - isoCode: "cs-CZ", + isoCode: isoCode, 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 }; \ No newline at end of file diff --git a/ui/src/keyboardLayouts/de_CH.ts b/ui/src/keyboardLayouts/de_CH.ts index 4743bcf..8776409 100644 --- a/ui/src/keyboardLayouts/de_CH.ts +++ b/ui/src/keyboardLayouts/de_CH.ts @@ -1,12 +1,15 @@ 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 keyAcute = { key: "Minus", altRight: true } // accent aigu (acute accent), mark ´ placed above the letter -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 keyTilde = { key: "Equal", altRight: true } // tilde, mark ~ placed above the letter +const name = "Schwiizerdütsch"; +const isoCode = "de-CH"; + +const keyTrema: KeyCombo = { key: "BracketRight" } // tréma (umlaut), two dots placed above a vowel +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 = { A: { key: "KeyA", shift: true }, @@ -164,8 +167,22 @@ const chars = { Tab: { key: "Tab" }, } as Record; +const keyDisplayMap = { + ...en_US.keyDisplayMap, + BracketLeft: "è", + "(BracketLeft)": "ü", + Semicolon: "é", + "(Semicolon)": "ö", + Quote: "à", + "(Quote)": "ä", +} as Record; + export const de_CH: KeyboardLayout = { - isoCode: "de-CH", + isoCode: isoCode, name: name, - chars: chars + chars: chars, + keyDisplayMap: keyDisplayMap, + // TODO need to localize these maps and layouts + modifierDisplayMap: en_US.modifierDisplayMap, + virtualKeyboard: en_US.virtualKeyboard }; \ No newline at end of file diff --git a/ui/src/keyboardLayouts/de_DE.ts b/ui/src/keyboardLayouts/de_DE.ts index 89b7eed..d05d542 100644 --- a/ui/src/keyboardLayouts/de_DE.ts +++ b/ui/src/keyboardLayouts/de_DE.ts @@ -1,113 +1,146 @@ 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 keyHat = { key: "Backquote" } // accent circonflexe (accent hat), mark ^ placed above the letter -const keyGrave = { key: "Equal", shift: true } // accent grave, mark ` placed above the letter +const name = "Deutsch"; +const isoCode = "de-DE"; + +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 = { + a: { key: "KeyA" }, + "á": { key: "KeyA", accentKey: keyAcute }, + "â": { key: "KeyA", accentKey: keyHat }, + "à": { key: "KeyA", accentKey: keyGrave }, A: { key: "KeyA", shift: true }, "Á": { key: "KeyA", shift: true, accentKey: keyAcute }, "Â": { key: "KeyA", shift: true, accentKey: keyHat }, "À": { key: "KeyA", shift: true, accentKey: keyGrave }, + "☺": { key: "KeyA", altRight: true }, // white smiling face ☺ + b: { key: "KeyB" }, B: { key: "KeyB", shift: true }, + "‹": { key: "KeyB", altRight: true }, // single left-pointing angle quotation mark, ‹ + c: { key: "KeyC" }, C: { key: "KeyC", shift: true }, + "\u202f": { key: "KeyC", altRight: true }, // narrow no-break space + d: { key: "KeyD" }, 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 }, "É": { key: "KeyE", shift: true, accentKey: keyAcute }, "Ê": { key: "KeyE", shift: true, accentKey: keyHat }, "È": { 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", shift: true }, + "˟": { key: "KeyF", deadKey: true, altRight: true }, // modifier letter cross accent, ˟ + G: { key: "KeyG", shift: true }, g: { key: "KeyG" }, + "ẞ": { key: "KeyG", altRight: true }, // capital sharp S, ẞ h: { key: "KeyH" }, + H: { key: "KeyH", shift: true }, + "ˍ": { key: "KeyH", deadKey: true, altRight: true }, // modifier letter low macron, ˍ i: { key: "KeyI" }, "í": { key: "KeyI", accentKey: keyAcute }, "î": { key: "KeyI", accentKey: keyHat }, "ì": { 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", shift: true }, + "¸": { key: "KeyJ", deadKey: true, altRight: true }, // cedilla accent, mark ¸ placed below the letter k: { key: "KeyK" }, + K: { key: "KeyK", shift: true }, l: { key: "KeyL" }, + L: { key: "KeyL", shift: true }, + "ˏ": { key: "KeyL", deadKey: true, altRight: true }, // modifier letter reversed comma, ˏ m: { key: "KeyM" }, + M: { key: "KeyM", shift: true }, "µ": { key: "KeyM", altRight: true }, n: { key: "KeyN" }, + N: { key: "KeyN", shift: true }, + "–": { key: "KeyN", altRight: true }, // en dash, – o: { key: "KeyO" }, "ó": { key: "KeyO", accentKey: keyAcute }, "ô": { key: "KeyO", accentKey: keyHat }, "ò": { 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", shift: true }, + "ˀ": { key: "KeyP", deadKey: true, altRight: true }, // modifier letter apostrophe, ʾ q: { key: "KeyQ" }, + Q: { key: "KeyQ", shift: true }, "@": { key: "KeyQ", altRight: true }, + R: { key: "KeyR", shift: true }, 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" }, + "″": { key: "KeyS", altRight: true }, // double prime, mark ″ placed above the letter + T: { key: "KeyT", shift: true }, t: { key: "KeyT" }, + "ˇ": { key: "KeyT", deadKey: true, altRight: true }, // caron/hacek accent, mark ˇ placed above the letter u: { key: "KeyU" }, "ú": { key: "KeyU", accentKey: keyAcute }, "û": { key: "KeyU", accentKey: keyHat }, "ù": { 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", shift: true }, + "«": { key: "KeyV", altRight: true }, // left-pointing double angle quotation mark, « 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", shift: true }, + "»": { key: "KeyX", altRight: true }, + // cross key between shift and y (aka OEM 102 key) y: { key: "KeyZ" }, + Y: { key: "KeyZ", shift: true }, + "›": { key: "KeyZ", altRight: true }, // single right-pointing angle quotation mark, › 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", deadKey: true }, + "|": { key: "Backquote", altRight: true }, 1: { key: "Digit1" }, "!": { key: "Digit1", shift: true }, + "’": { key: "Digit1", altRight: true }, // single quote, mark ’ placed above the letter 2: { key: "Digit2" }, "\"": { key: "Digit2", shift: true }, "²": { key: "Digit2", altRight: true }, + "<": { key: "Digit2", altRight: true }, // non-US < and > 3: { key: "Digit3" }, "§": { key: "Digit3", shift: true }, "³": { key: "Digit3", altRight: true }, + ">": { key: "Digit3", altRight: true }, // non-US < and > 4: { key: "Digit4" }, "$": { key: "Digit4", shift: true }, + "—": { key: "Digit4", altRight: true }, // em dash, — 5: { key: "Digit5" }, "%": { key: "Digit5", shift: true }, + "¡": { key: "Digit5", altRight: true }, // inverted exclamation mark, ¡ 6: { key: "Digit6" }, "&": { key: "Digit6", shift: true }, + "¿": { key: "Digit6", altRight: true }, // inverted question mark, ¿ 7: { key: "Digit7" }, "/": { key: "Digit7", shift: true }, "{": { key: "Digit7", altRight: true }, @@ -123,36 +156,192 @@ const chars = { "ß": { key: "Minus" }, "?": { key: "Minus", shift: true }, "\\": { key: "Minus", altRight: true }, - "´": { key: "Equal", deadKey: true }, - "`": { key: "Equal", shift: true, deadKey: true }, + "´": { key: "Equal", deadKey: true }, // accent acute, mark ´ placed above the letter + "`": { 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", shift: true }, + Escape: { key: "BracketLeft", control: true }, + "ʼ": { key: "BracketLeft", altRight: true }, // modifier letter apostrophe, ʼ "+": { key: "BracketRight" }, "*": { key: "BracketRight", shift: true }, + Control: { key: "BracketRight", control: true }, "~": { key: "BracketRight", altRight: true }, "ö": { key: "Semicolon" }, "Ö": { key: "Semicolon", shift: true }, + "ˌ": { key: "Semicolon", deadkey: true, altRight: true }, // modifier letter low vertical line, ˌ "ä": { key: "Quote" }, "Ä": { key: "Quote", shift: true }, + "˗": { key: "Quote", deadKey: true, altRight: true }, // modifier letter minus sign, ˗ "#": { key: "Backslash" }, "'": { key: "Backslash", shift: true }, + "−": { key: "Backslash", altRight: true }, // minus sign, − ",": { key: "Comma" }, ";": { key: "Comma", shift: true }, + "\u2011": { key: "Comma", altRight: true }, // non-breaking hyphen, ‑ ".": { key: "Period" }, ":": { key: "Period", shift: true }, + "·": { key: "Period", altRight: true }, // middle dot, · "-": { key: "Slash" }, "_": { key: "Slash", shift: true }, - "<": { key: "IntlBackslash" }, - ">": { key: "IntlBackslash", shift: true }, - "|": { key: "IntlBackslash", altRight: true }, + "\u00ad": { key: "Slash", altRight: true }, // soft hyphen, ­ " ": { key: "Space" }, "\n": { key: "Enter" }, Enter: { key: "Enter" }, Tab: { key: "Tab" }, } as Record; +export const keyDisplayMap: Record = { + ...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 = { + 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; + +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 = { - isoCode: "de-DE", + isoCode: isoCode, name: name, - chars: chars + chars: chars, + keyDisplayMap: keyDisplayMap, + modifierDisplayMap: modifierDisplayMap, + virtualKeyboard: virtualKeyboard }; \ No newline at end of file diff --git a/ui/src/keyboardLayouts/en_UK.ts b/ui/src/keyboardLayouts/en_UK.ts index a5ef779..5341f0f 100644 --- a/ui/src/keyboardLayouts/en_UK.ts +++ b/ui/src/keyboardLayouts/en_UK.ts @@ -1,6 +1,9 @@ import { KeyboardLayout, KeyCombo } from "../keyboardLayouts" +import { en_US } from "./en_US" // for fallback of keyDisplayMap, modifierDisplayMap, and virtualKeyboard + const name = "English (UK)"; +const isoCode = "en-UK"; const chars = { A: { key: "KeyA", shift: true }, @@ -107,7 +110,11 @@ const chars = { } as Record export const en_UK: KeyboardLayout = { - isoCode: "en-UK", + isoCode: isoCode, 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 }; \ No newline at end of file diff --git a/ui/src/keyboardLayouts/en_US.ts b/ui/src/keyboardLayouts/en_US.ts index cd7aaf6..872d356 100644 --- a/ui/src/keyboardLayouts/en_US.ts +++ b/ui/src/keyboardLayouts/en_US.ts @@ -1,8 +1,18 @@ import { KeyboardLayout, KeyCombo } from "../keyboardLayouts" 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 }, B: { key: "KeyB", shift: true }, C: { key: "KeyC", shift: true }, @@ -89,31 +99,213 @@ const chars = { ">": { key: "Period", shift: true }, ";": { key: "Semicolon" }, ":": { key: "Semicolon", shift: true }, + "¶": { key: "Semicolon", altRight: true }, // pilcrow sign "[": { key: "BracketLeft" }, "{": { key: "BracketLeft", shift: true }, + "«": { key: "BracketLeft", altRight: true }, // double left quote sign "]": { key: "BracketRight" }, "}": { key: "BracketRight", shift: true }, + "»": { key: "BracketRight", altRight: true }, // double right quote sign "\\": { key: "Backslash" }, "|": { key: "Backslash", shift: true }, + "¬": { key: "Backslash", altRight: true }, // not sign "`": { key: "Backquote" }, "~": { key: "Backquote", shift: true }, "§": { key: "IntlBackslash" }, "±": { key: "IntlBackslash", shift: true }, - " ": { key: "Space", shift: false }, - "\n": { key: "Enter", shift: false }, - Enter: { key: "Enter", shift: false }, - Tab: { key: "Tab", shift: false }, - PrintScreen: { key: "Prt Sc", shift: false }, + " ": { key: "Space" }, + "\n": { key: "Enter" }, + Enter: { key: "Enter" }, + Escape: { key: "Escape" }, + Tab: { key: "Tab" }, + PrintScreen: { key: "Prt Sc" }, SystemRequest: { key: "Prt Sc", shift: true }, - ScrollLock: { key: "ScrollLock", shift: false}, - Pause: { key: "Pause", shift: false }, + ScrollLock: { key: "ScrollLock" }, + Pause: { key: "Pause" }, Break: { key: "Pause", shift: true }, - Insert: { key: "Insert", shift: false }, - Delete: { key: "Delete", shift: false }, + Insert: { key: "Insert" }, + Delete: { key: "Delete" }, } as Record +export const modifierDisplayMap: Record = { + ControlLeft: "Left Ctrl", + ControlRight: "Right Ctrl", + ShiftLeft: "Left Shift", + ShiftRight: "Right Shift", + AltLeft: "Left Alt", + AltRight: "Right Alt", + MetaLeft: "Left Meta", + MetaRight: "Right Meta", + AltGr: "AltGr", +} as Record; + +export const keyDisplayMap: Record = { + 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: "en-US", - name: name, - chars: chars -}; \ No newline at end of file + isoCode, + name, + chars, + keyDisplayMap, + modifierDisplayMap, + virtualKeyboard +}; + + diff --git a/ui/src/keyboardLayouts/es_ES.ts b/ui/src/keyboardLayouts/es_ES.ts index 9eb1d6a..ab7762b 100644 --- a/ui/src/keyboardLayouts/es_ES.ts +++ b/ui/src/keyboardLayouts/es_ES.ts @@ -1,12 +1,15 @@ 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 keyAcute = { key: "Quote" } // accent aigu (acute accent), mark ´ placed above the letter -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 keyTilde = { key: "Key4", altRight: true } // tilde, mark ~ placed above the letter +const name = "Español"; +const isoCode = "es-ES"; + +const keyTrema: KeyCombo = { key: "Quote", shift: true } // tréma (umlaut), two dots placed above a vowel +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 = { A: { key: "KeyA", shift: true }, @@ -168,7 +171,11 @@ const chars = { } as Record; export const es_ES: KeyboardLayout = { - isoCode: "es-ES", + isoCode: isoCode, 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 }; \ No newline at end of file diff --git a/ui/src/keyboardLayouts/fr_BE.ts b/ui/src/keyboardLayouts/fr_BE.ts index bd417e0..fb5a79b 100644 --- a/ui/src/keyboardLayouts/fr_BE.ts +++ b/ui/src/keyboardLayouts/fr_BE.ts @@ -1,12 +1,15 @@ 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 keyHat = { key: "BracketLeft" } // accent circonflexe (accent hat), mark ^ placed above the letter -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 keyTilde = { key: "Slash", altRight: true } // tilde, mark ~ placed above the letter +const name = "Belgisch Nederlands"; +const isoCode = "nl-BE"; + +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 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 = { A: { key: "KeyQ", shift: true }, @@ -167,7 +170,11 @@ const chars = { } as Record; export const fr_BE: KeyboardLayout = { - isoCode: "fr-BE", + isoCode: isoCode, 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 }; \ No newline at end of file diff --git a/ui/src/keyboardLayouts/fr_CH.ts b/ui/src/keyboardLayouts/fr_CH.ts index 0ba8cb4..d0a70f3 100644 --- a/ui/src/keyboardLayouts/fr_CH.ts +++ b/ui/src/keyboardLayouts/fr_CH.ts @@ -3,6 +3,7 @@ import { KeyboardLayout, KeyCombo } from "../keyboardLayouts" import { de_CH } from "./de_CH" const name = "Français de Suisse"; +const isoCode = "fr-CH"; const chars = { ...de_CH.chars, @@ -14,8 +15,22 @@ const chars = { "ä": { key: "Quote", shift: true }, } as Record; +const keyDisplayMap = { + ...de_CH.keyDisplayMap, + "BracketLeft": "è", + "BracketLeftShift": "ü", + "Semicolon": "é", + "SemicolonShift": "ö", + "Quote": "à", + "QuoteShift": "ä", +} as Record; + export const fr_CH: KeyboardLayout = { - isoCode: "fr-CH", + isoCode: isoCode, name: name, - chars: chars + chars: chars, + keyDisplayMap: keyDisplayMap, + // TODO need to localize these maps and layouts + modifierDisplayMap: de_CH.modifierDisplayMap, + virtualKeyboard: de_CH.virtualKeyboard }; diff --git a/ui/src/keyboardLayouts/fr_FR.ts b/ui/src/keyboardLayouts/fr_FR.ts index 29d5104..2ac5e74 100644 --- a/ui/src/keyboardLayouts/fr_FR.ts +++ b/ui/src/keyboardLayouts/fr_FR.ts @@ -1,9 +1,12 @@ 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 keyHat = { key: "BracketLeft" } // accent circonflexe (accent hat), mark ^ placed above the letter +const name = "Français"; +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 = { A: { key: "KeyQ", shift: true }, @@ -139,7 +142,11 @@ const chars = { } as Record; export const fr_FR: KeyboardLayout = { - isoCode: "fr-FR", + isoCode: isoCode, 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 }; \ No newline at end of file diff --git a/ui/src/keyboardLayouts/it_IT.ts b/ui/src/keyboardLayouts/it_IT.ts index 0ff6e24..160b0fc 100644 --- a/ui/src/keyboardLayouts/it_IT.ts +++ b/ui/src/keyboardLayouts/it_IT.ts @@ -1,6 +1,9 @@ import { KeyboardLayout, KeyCombo } from "../keyboardLayouts" +import { en_US } from "./en_US" // for fallback of keyDisplayMap, modifierDisplayMap, and virtualKeyboard + const name = "Italiano"; +const isoCode = "it-IT"; const chars = { A: { key: "KeyA", shift: true }, @@ -113,7 +116,11 @@ const chars = { } as Record; export const it_IT: KeyboardLayout = { - isoCode: "it-IT", + isoCode: isoCode, 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 }; \ No newline at end of file diff --git a/ui/src/keyboardLayouts/nb_NO.ts b/ui/src/keyboardLayouts/nb_NO.ts index 4dae9c8..25043d9 100644 --- a/ui/src/keyboardLayouts/nb_NO.ts +++ b/ui/src/keyboardLayouts/nb_NO.ts @@ -1,12 +1,15 @@ 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 keyAcute = { key: "Equal", altRight: true } // accent aigu (acute accent), mark ´ placed above the letter -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 keyTilde = { key: "BracketRight", altRight: true } // tilde, mark ~ placed above the letter +const name = "Norsk bokmål"; +const isoCode = "nb-NO"; + +const keyTrema: KeyCombo = { key: "BracketRight" } // tréma (umlaut), two dots placed above a vowel +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 = { A: { key: "KeyA", shift: true }, @@ -167,7 +170,11 @@ const chars = { } as Record; export const nb_NO: KeyboardLayout = { - isoCode: "nb-NO", + isoCode: isoCode, 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 }; \ No newline at end of file diff --git a/ui/src/keyboardLayouts/sv_SE.ts b/ui/src/keyboardLayouts/sv_SE.ts index fbde3d0..388ddf9 100644 --- a/ui/src/keyboardLayouts/sv_SE.ts +++ b/ui/src/keyboardLayouts/sv_SE.ts @@ -1,12 +1,15 @@ 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 keyAcute = { key: "Equal" } // accent aigu (acute accent), mark ´ placed above the letter -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 keyTilde = { key: "BracketRight", altRight: true } // tilde, mark ~ placed above the letter +const name = "Svenska"; +const isoCode = "sv-SE"; + +const keyTrema: KeyCombo = { key: "BracketRight" } // tréma (umlaut), two dots placed above a vowel +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 = { A: { key: "KeyA", shift: true }, @@ -164,7 +167,11 @@ const chars = { } as Record; export const sv_SE: KeyboardLayout = { - isoCode: "sv-SE", + isoCode: isoCode, 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 }; \ No newline at end of file diff --git a/ui/src/keyboardMappings.ts b/ui/src/keyboardMappings.ts index bb24fbb..14b0c60 100644 --- a/ui/src/keyboardMappings.ts +++ b/ui/src/keyboardMappings.ts @@ -1,20 +1,39 @@ // Key codes and modifiers correspond to definitions in the // [Linux USB HID gadget driver](https://www.kernel.org/doc/Documentation/usb/gadget_hid.txt) -// [Section 10. Keyboard/Keypad Page 0x07](https://usb.org/sites/default/files/hut1_21.pdf) +// [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 = { + Again: 0x79, + AlternateErase: 0x9d, + AltGr: 0xe6, // aka AltRight + AltLeft: 0xe2, + AltRight: 0xe6, + Application: 0x65, ArrowDown: 0x51, ArrowLeft: 0x50, ArrowRight: 0x4f, ArrowUp: 0x52, + Attention: 0x9a, Backquote: 0x35, // aka Grave Backslash: 0x31, Backspace: 0x2a, BracketLeft: 0x2f, // aka LeftBrace BracketRight: 0x30, // aka RightBrace + Cancel: 0x9b, CapsLock: 0x39, + Clear: 0x9c, + ClearAgain: 0xa2, Comma: 0x36, - Compose: 0x65, - ContextMenu: 0, + Compose: 0xe3, + ContextMenu: 0x65, + ControlLeft: 0xe0, + ControlRight: 0xe4, + Copy: 0x7c, + CrSel: 0xa3, + CurrencySubunit: 0xb5, + CurrencyUnit: 0xb4, + Cut: 0x7b, + DecimalSeparator: 0xb3, Delete: 0x4c, Digit0: 0x27, Digit1: 0x1e, @@ -30,6 +49,8 @@ export const keys = { Enter: 0x28, Equal: 0x2e, Escape: 0x29, + Execute: 0x74, + ExSel: 0xa4, F1: 0x3a, F2: 0x3b, F3: 0x3c, @@ -42,6 +63,7 @@ export const keys = { F10: 0x43, F11: 0x44, F12: 0x45, + F13: 0x68, F14: 0x69, F15: 0x6a, F16: 0x6b, @@ -53,9 +75,21 @@ export const keys = { F22: 0x71, F23: 0x72, F24: 0x73, - Home: 0x4a, + Find: 0x7e, + Grave: 0x35, HashTilde: 0x32, // non-US # and ~ + Help: 0x75, + Home: 0x4a, 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 | KeyA: 0x04, KeyB: 0x05, @@ -83,11 +117,27 @@ export const keys = { KeyX: 0x1b, KeyY: 0x1c, 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, - None: 0x00, + Mute: 0x7f, NumLock: 0x53, // and Clear Numpad0: 0x62, // and Insert + Numpad00: 0xb0, + Numpad000: 0xb1, Numpad1: 0x59, // and End Numpad2: 0x5a, // and Down Arrow Numpad3: 0x5b, // and Page Down @@ -98,30 +148,111 @@ export const keys = { Numpad8: 0x60, // and Up Arrow Numpad9: 0x61, // and Page Up NumpadAdd: 0x57, + NumpadAnd: 0xc7, + NumpadAt: 0xce, + NumpadBackspace: 0xbb, + NumpadBinary: 0xda, + NumpadCircumflex: 0xc3, + NumpadClear: 0xd8, + NumpadClearEntry: 0xd9, + NumpadColon: 0xcb, NumpadComma: 0x85, NumpadDecimal: 0x63, + NumpadDecimalBase: 0xdc, + NumpadDelete: 0x63, NumpadDivide: 0x54, + NumpadDownArrow: 0x5a, + NumpadEnd: 0x59, NumpadEnter: 0x58, 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, + NumpadLessThan: 0xc5, + NumpadLogicalAnd: 0xc8, + NumpadLogicalOr: 0xca, + NumpadMemoryAdd: 0xd3, + NumpadMemoryClear: 0xd2, + NumpadMemoryDivide: 0xd6, + NumpadMemoryMultiply: 0xd5, + NumpadMemoryRecall: 0xd1, + NumpadMemoryStore: 0xd0, + NumpadMemorySubtract: 0xd4, NumpadMultiply: 0x55, + NumpadOctal: 0xdb, + NumpadOctathorpe: 0xcc, + NumpadOr: 0xc9, + NumpadPageDown: 0x5b, + NumpadPageUp: 0x61, + NumpadPercent: 0xc4, + NumpadPlusMinus: 0xd7, + NumpadRightArrow: 0x5e, + NumpadRightBrace: 0xb9, NumpadRightParen: 0xb7, + NumpadSpace: 0xcd, NumpadSubtract: 0x56, + NumpadTab: 0xba, + NumpadUpArrow: 0x60, + NumpadXOR: 0xc2, + Octothorpe: 0x32, // non-US # and ~ + Operation: 0xa1, + Out: 0xa0, PageDown: 0x4e, PageUp: 0x4b, - Period: 0x37, - PrintScreen: 0x46, + Paste: 0x7d, Pause: 0x48, + Period: 0x37, Power: 0x66, + PrintScreen: 0x46, + Prior: 0x9d, Quote: 0x34, // aka Single Quote or Apostrophe + Return: 0x9e, ScrollLock: 0x47, + Select: 0x77, Semicolon: 0x33, + Separator: 0x9f, + ShiftLeft: 0xe1, + ShiftRight: 0xe5, Slash: 0x38, Space: 0x2c, + Stop: 0x78, SystemRequest: 0x9a, Tab: 0x2b, + ThousandsSeparator: 0xb2, + Tilde: 0x35, + Undo: 0x7a, + VolumeDown: 0x81, + VolumeUp: 0x80, } as Record; +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 + export const modifiers = { ControlLeft: 0x01, ControlRight: 0x10, @@ -131,113 +262,28 @@ export const modifiers = { AltRight: 0x40, MetaLeft: 0x08, MetaRight: 0x80, + AltGr: 0x40, } as Record; -export const modifierDisplayMap: Record = { - ControlLeft: "Left Ctrl", - ControlRight: "Right Ctrl", - ShiftLeft: "Left Shift", - ShiftRight: "Right Shift", - AltLeft: "Left Alt", - AltRight: "Right Alt", - MetaLeft: "Left Meta", - MetaRight: "Right Meta", -} as Record; +export const hidKeyToModifierMask = { + 0xe0: modifiers.ControlLeft, + 0xe1: modifiers.ShiftLeft, + 0xe2: modifiers.AltLeft, + 0xe3: modifiers.MetaLeft, + 0xe4: modifiers.ControlRight, + 0xe5: modifiers.ShiftRight, + 0xe6: modifiers.AltRight, // can also be AltGr + 0xe7: modifiers.MetaRight, +} as Record; -export const keyDisplayMap: Record = { - 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 - 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", +export const latchingKeys = ["CapsLock", "ScrollLock", "NumLock", "Meta", "Compose", "Kana"]; - // 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" -}; +export function decodeModifiers(modifier: number) { + return { + isShiftActive: (modifier & (modifiers.ShiftLeft | modifiers.ShiftRight)) !== 0, + isControlActive: (modifier & (modifiers.ControlLeft | modifiers.ControlRight)) !== 0, + isAltActive: (modifier & (modifiers.AltLeft | modifiers.AltRight)) !== 0, + isMetaActive: (modifier & (modifiers.MetaLeft | modifiers.MetaRight)) !== 0, + isAltGrActive: (modifier & modifiers.AltGr) !== 0, + }; +} \ No newline at end of file diff --git a/ui/src/routes/devices.$id.mount.tsx b/ui/src/routes/devices.$id.mount.tsx index 7297e40..34fb919 100644 --- a/ui/src/routes/devices.$id.mount.tsx +++ b/ui/src/routes/devices.$id.mount.tsx @@ -64,7 +64,7 @@ export function Dialog({ onClose }: { onClose: () => void }) { setRemoteVirtualMediaState(null); } - const [send] = useJsonRpc(); + const { send } = useJsonRpc(); async function syncRemoteVirtualMediaState() { return new Promise((resolve, reject) => { send("getVirtualMediaState", {}, resp => { @@ -689,7 +689,7 @@ function DeviceFileView({ const [currentPage, setCurrentPage] = useState(1); const filesPerPage = 5; - const [send] = useJsonRpc(); + const { send } = useJsonRpc(); interface StorageSpace { bytesUsed: number; @@ -1001,7 +1001,7 @@ function UploadFileView({ const [fileError, setFileError] = useState(null); const [uploadError, setUploadError] = useState(null); - const [send] = useJsonRpc(); + const { send } = useJsonRpc(); const rtcDataChannelRef = useRef(null); useEffect(() => { diff --git a/ui/src/routes/devices.$id.settings.access._index.tsx b/ui/src/routes/devices.$id.settings.access._index.tsx index e0543b8..951c07b 100644 --- a/ui/src/routes/devices.$id.settings.access._index.tsx +++ b/ui/src/routes/devices.$id.settings.access._index.tsx @@ -42,7 +42,7 @@ export default function SettingsAccessIndexRoute() { const { navigateTo } = useDeviceUiNavigation(); const navigate = useNavigate(); - const [send] = useJsonRpc(); + const { send } = useJsonRpc(); const [isAdopted, setAdopted] = useState(false); const [deviceId, setDeviceId] = useState(null); @@ -166,9 +166,7 @@ export default function SettingsAccessIndexRoute() { notifications.success("TLS settings updated successfully"); }); - }, - [send], - ); + }, [send]); // Handle TLS mode change const handleTlsModeChange = (value: string) => { diff --git a/ui/src/routes/devices.$id.settings.advanced.tsx b/ui/src/routes/devices.$id.settings.advanced.tsx index d1dab68..c453e79 100644 --- a/ui/src/routes/devices.$id.settings.advanced.tsx +++ b/ui/src/routes/devices.$id.settings.advanced.tsx @@ -15,10 +15,10 @@ import notifications from "../notifications"; import { SettingsItem } from "./devices.$id.settings"; export default function SettingsAdvancedRoute() { - const [send] = useJsonRpc(); + const { send } = useJsonRpc(); const [sshKey, setSSHKey] = useState(""); - const setDeveloperMode = useSettingsStore(state => state.setDeveloperMode); + const { setDeveloperMode } = useSettingsStore(); const [devChannel, setDevChannel] = useState(false); const [usbEmulationEnabled, setUsbEmulationEnabled] = useState(false); const [showLoopbackWarning, setShowLoopbackWarning] = useState(false); diff --git a/ui/src/routes/devices.$id.settings.general._index.tsx b/ui/src/routes/devices.$id.settings.general._index.tsx index ecefdfa..8916af4 100644 --- a/ui/src/routes/devices.$id.settings.general._index.tsx +++ b/ui/src/routes/devices.$id.settings.general._index.tsx @@ -13,7 +13,7 @@ import { useDeviceStore } from "../hooks/stores"; import { SettingsItem } from "./devices.$id.settings"; export default function SettingsGeneralRoute() { - const [send] = useJsonRpc(); + const { send } = useJsonRpc(); const { navigateTo } = useDeviceUiNavigation(); const [autoUpdate, setAutoUpdate] = useState(true); diff --git a/ui/src/routes/devices.$id.settings.general.reboot.tsx b/ui/src/routes/devices.$id.settings.general.reboot.tsx index c6889f6..0bf114c 100644 --- a/ui/src/routes/devices.$id.settings.general.reboot.tsx +++ b/ui/src/routes/devices.$id.settings.general.reboot.tsx @@ -6,7 +6,7 @@ import { Button } from "@components/Button"; export default function SettingsGeneralRebootRoute() { const navigate = useNavigate(); - const [send] = useJsonRpc(); + const { send } = useJsonRpc(); const onConfirmUpdate = useCallback(() => { // This is where we send the RPC to the golang binary diff --git a/ui/src/routes/devices.$id.settings.general.update.tsx b/ui/src/routes/devices.$id.settings.general.update.tsx index 7c41449..f456d89 100644 --- a/ui/src/routes/devices.$id.settings.general.update.tsx +++ b/ui/src/routes/devices.$id.settings.general.update.tsx @@ -16,7 +16,7 @@ export default function SettingsGeneralUpdateRoute() { const { updateSuccess } = location.state || {}; const { setModalView, otaState } = useUpdateStore(); - const [send] = useJsonRpc(); + const { send } = useJsonRpc(); const onConfirmUpdate = useCallback(() => { send("tryUpdate", {}); @@ -134,10 +134,8 @@ function LoadingState({ }) { const [progressWidth, setProgressWidth] = useState("0%"); const abortControllerRef = useRef(null); - const [send] = useJsonRpc(); - - const setAppVersion = useDeviceStore(state => state.setAppVersion); - const setSystemVersion = useDeviceStore(state => state.setSystemVersion); + const { send } = useJsonRpc(); + const { setAppVersion, setSystemVersion } = useDeviceStore(); const getVersionInfo = useCallback(() => { return new Promise((resolve, reject) => { diff --git a/ui/src/routes/devices.$id.settings.hardware.tsx b/ui/src/routes/devices.$id.settings.hardware.tsx index 82cc6a1..850126c 100644 --- a/ui/src/routes/devices.$id.settings.hardware.tsx +++ b/ui/src/routes/devices.$id.settings.hardware.tsx @@ -12,10 +12,9 @@ import { UsbInfoSetting } from "../components/UsbInfoSetting"; import { FeatureFlag } from "../components/FeatureFlag"; export default function SettingsHardwareRoute() { - const [send] = useJsonRpc(); + const { send } = useJsonRpc(); const settings = useSettingsStore(); - - const setDisplayRotation = useSettingsStore(state => state.setDisplayRotation); + const { setDisplayRotation } = useSettingsStore(); const handleDisplayRotationChange = (rotation: string) => { setDisplayRotation(rotation); @@ -34,7 +33,7 @@ export default function SettingsHardwareRoute() { }); }; - const setBacklightSettings = useSettingsStore(state => state.setBacklightSettings); + const { setBacklightSettings } = useSettingsStore(); const handleBacklightSettingsChange = (settings: BacklightSettings) => { // If the user has set the display to dim after it turns off, set the dim_after diff --git a/ui/src/routes/devices.$id.settings.keyboard.tsx b/ui/src/routes/devices.$id.settings.keyboard.tsx index 57119ba..8a158a7 100644 --- a/ui/src/routes/devices.$id.settings.keyboard.tsx +++ b/ui/src/routes/devices.$id.settings.keyboard.tsx @@ -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 notifications from "@/notifications"; +import useKeyboardLayout from "@/hooks/useKeyboardLayout"; import { SettingsPageHeader } from "@components/SettingsPageheader"; -import { keyboardOptions } from "@/keyboardLayouts"; 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"; export default function SettingsKeyboardRoute() { - const keyboardLayout = useSettingsStore(state => state.keyboardLayout); - const keyboardLedSync = useSettingsStore(state => state.keyboardLedSync); - const showPressedKeys = useSettingsStore(state => state.showPressedKeys); - const setKeyboardLayout = useSettingsStore( - state => state.setKeyboardLayout, - ); - const setKeyboardLedSync = useSettingsStore( - state => state.setKeyboardLedSync, - ); - const setShowPressedKeys = useSettingsStore( - state => state.setShowPressedKeys, - ); + const { setKeyboardLayout } = useSettingsStore(); + const { showPressedKeys, setShowPressedKeys } = useSettingsStore(); + const { selectedKeyboard, keyboardOptions } = useKeyboardLayout(); - // 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]); - - const layoutOptions = keyboardOptions(); - const ledSyncOptions = [ - { value: "auto", label: "Automatic" }, - { value: "browser", label: "Browser Only" }, - { value: "host", label: "Host Only" }, - ]; - - const [send] = useJsonRpc(); + const { send } = useJsonRpc(); useEffect(() => { send("getKeyboardLayout", {}, resp => { 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]); const onKeyboardLayoutChange = useCallback( (e: React.ChangeEvent) => { - const layout = e.target.value; - send("setKeyboardLayout", { layout }, resp => { + const isoCode = e.target.value; + send("setKeyboardLayout", { layout: isoCode }, resp => { if ("error" in resp) { notifications.error( `Failed to set keyboard layout: ${resp.error.data || "Unknown error"}`, ); } - notifications.success("Keyboard layout set successfully"); - setKeyboardLayout(layout); + notifications.success("Keyboard layout set successfully to " + isoCode); + setKeyboardLayout(isoCode); }); }, [send, setKeyboardLayout], @@ -72,7 +52,6 @@ export default function SettingsKeyboardRoute() { />
- { /* this menu item could be renamed to plain "Keyboard layout" in the future, when also the virtual keyboard layout mappings are being implemented */ }

@@ -91,23 +70,6 @@ export default function SettingsKeyboardRoute() {

-
- { /* this menu item could be renamed to plain "Keyboard layout" in the future, when also the virtual keyboard layout mappings are being implemented */ } - - setKeyboardLedSync(e.target.value as KeyboardLedSync)} - options={ledSyncOptions} - /> - -
-
{ return macros.map((macro, index) => ({ @@ -35,6 +35,7 @@ export default function SettingsMacrosRoute() { const [actionLoadingId, setActionLoadingId] = useState(null); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const [macroToDelete, setMacroToDelete] = useState(null); + const { selectedKeyboard } = useKeyboardLayout(); const isMaxMacrosReached = useMemo( () => macros.length >= MAX_TOTAL_MACROS, @@ -185,7 +186,7 @@ export default function SettingsMacrosRoute() { step.modifiers.map((modifier, idx) => ( - {modifierDisplayMap[modifier] || modifier} + {selectedKeyboard.modifierDisplayMap[modifier] || modifier} {idx < step.modifiers.length - 1 && ( @@ -210,7 +211,7 @@ export default function SettingsMacrosRoute() { step.keys.map((key, idx) => ( - {keyDisplayMap[key] || key} + {selectedKeyboard.keyDisplayMap[key] || key} {idx < step.keys.length - 1 && ( @@ -297,8 +298,10 @@ export default function SettingsMacrosRoute() { actionLoadingId, handleDeleteMacro, handleMoveMacro, + selectedKeyboard.modifierDisplayMap, + selectedKeyboard.keyDisplayMap, handleDuplicateMacro, - navigate, + navigate ], ); diff --git a/ui/src/routes/devices.$id.settings.mouse.tsx b/ui/src/routes/devices.$id.settings.mouse.tsx index f5ea6aa..2de2e17 100644 --- a/ui/src/routes/devices.$id.settings.mouse.tsx +++ b/ui/src/routes/devices.$id.settings.mouse.tsx @@ -64,14 +64,11 @@ const jigglerOptions = [ type JigglerValues = (typeof jigglerOptions)[number]["value"] | "custom"; export default function SettingsMouseRoute() { - const hideCursor = useSettingsStore(state => state.isCursorHidden); - const setHideCursor = useSettingsStore(state => state.setCursorVisibility); - - const mouseMode = useSettingsStore(state => state.mouseMode); - const setMouseMode = useSettingsStore(state => state.setMouseMode); - - const scrollThrottling = useSettingsStore(state => state.scrollThrottling); - const setScrollThrottling = useSettingsStore(state => state.setScrollThrottling); + const { + isCursorHidden, setCursorVisibility, + mouseMode, setMouseMode, + scrollThrottling, setScrollThrottling + } = useSettingsStore(); const [selectedJigglerOption, setSelectedJigglerOption] = useState(null); @@ -87,7 +84,7 @@ export default function SettingsMouseRoute() { { value: "100", label: "Very High" }, ]; - const [send] = useJsonRpc(); + const { send } = useJsonRpc(); const syncJigglerSettings = useCallback(() => { send("getJigglerState", {}, resp => { @@ -196,8 +193,8 @@ export default function SettingsMouseRoute() { description="Hide the cursor when sending mouse movements" > setHideCursor(e.target.checked)} + checked={isCursorHidden} + onChange={e => setCursorVisibility(e.target.checked)} /> diff --git a/ui/src/routes/devices.$id.settings.network.tsx b/ui/src/routes/devices.$id.settings.network.tsx index 6fcd588..ebcbc85 100644 --- a/ui/src/routes/devices.$id.settings.network.tsx +++ b/ui/src/routes/devices.$id.settings.network.tsx @@ -72,7 +72,7 @@ export function LifeTimeLabel({ lifetime }: { lifetime: string }) { } export default function SettingsNetworkRoute() { - const [send] = useJsonRpc(); + const { send } = useJsonRpc(); const [networkState, setNetworkState] = useNetworkStateStore(state => [ state, state.setNetworkState, @@ -106,11 +106,12 @@ export default function SettingsNetworkRoute() { setNetworkSettingsLoaded(false); send("getNetworkSettings", {}, resp => { if ("error" in resp) return; - console.log(resp.result); - setNetworkSettings(resp.result as NetworkSettings); + const networkSettings = resp.result as NetworkSettings; + console.debug("Network settings: ", networkSettings); + setNetworkSettings(networkSettings); if (!firstNetworkSettings.current) { - firstNetworkSettings.current = resp.result as NetworkSettings; + firstNetworkSettings.current = networkSettings; } setNetworkSettingsLoaded(true); }); @@ -119,8 +120,9 @@ export default function SettingsNetworkRoute() { const getNetworkState = useCallback(() => { send("getNetworkState", {}, resp => { if ("error" in resp) return; - console.log(resp.result); - setNetworkState(resp.result as NetworkState); + const networkState = resp.result as NetworkState; + console.debug("Network state:", networkState); + setNetworkState(networkState); }); }, [send, setNetworkState]); @@ -136,9 +138,10 @@ export default function SettingsNetworkRoute() { setNetworkSettingsLoaded(true); 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 - firstNetworkSettings.current = resp.result as NetworkSettings; - setNetworkSettings(resp.result as NetworkSettings); + firstNetworkSettings.current = networkSettings; + setNetworkSettings(networkSettings); getNetworkState(); setNetworkSettingsLoaded(true); notifications.success("Network settings saved"); diff --git a/ui/src/routes/devices.$id.settings.tsx b/ui/src/routes/devices.$id.settings.tsx index 5075ab5..5a61778 100644 --- a/ui/src/routes/devices.$id.settings.tsx +++ b/ui/src/routes/devices.$id.settings.tsx @@ -17,19 +17,16 @@ import { useResizeObserver } from "usehooks-ts"; import Card from "@/components/Card"; import { LinkButton } from "@/components/Button"; +import { FeatureFlag } from "@/components/FeatureFlag"; import LoadingSpinner from "@/components/LoadingSpinner"; import { useUiStore } from "@/hooks/stores"; -import useKeyboard from "@/hooks/useKeyboard"; -import { FeatureFlag } from "../components/FeatureFlag"; 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. */ export default function SettingsRoute() { const location = useLocation(); - const setDisableVideoFocusTrap = useUiStore(state => state.setDisableVideoFocusTrap); - const { sendKeyboardEvent } = useKeyboard(); + const { setDisableVideoFocusTrap } = useUiStore(); const scrollContainerRef = useRef(null); const [showLeftGradient, setShowLeftGradient] = useState(false); const [showRightGradient, setShowRightGradient] = useState(false); @@ -65,21 +62,14 @@ export default function SettingsRoute() { }, [width]); useEffect(() => { - // disable focus trap setTimeout(() => { - // Reset keyboard state. Incase the user is pressing a key while enabling the sidebar - sendKeyboardEvent([], []); setDisableVideoFocusTrap(true); - // For some reason, the focus trap is not disabled immediately - // so we need to blur the active element - (document.activeElement as HTMLElement)?.blur(); - console.log("Just disabled focus trap"); - }, 300); + }, 500); return () => { setDisableVideoFocusTrap(false); }; - }, [sendKeyboardEvent, setDisableVideoFocusTrap]); + }, [setDisableVideoFocusTrap]); return (
diff --git a/ui/src/routes/devices.$id.settings.video.tsx b/ui/src/routes/devices.$id.settings.video.tsx index 9e888ab..e6a39ea 100644 --- a/ui/src/routes/devices.$id.settings.video.tsx +++ b/ui/src/routes/devices.$id.settings.video.tsx @@ -41,18 +41,17 @@ const streamQualityOptions = [ ]; export default function SettingsVideoRoute() { - const [send] = useJsonRpc(); + const { send } = useJsonRpc(); const [streamQuality, setStreamQuality] = useState("1"); const [customEdidValue, setCustomEdidValue] = useState(null); const [edid, setEdid] = useState(null); // Video enhancement settings from store - const videoSaturation = useSettingsStore(state => state.videoSaturation); - const setVideoSaturation = useSettingsStore(state => state.setVideoSaturation); - const videoBrightness = useSettingsStore(state => state.videoBrightness); - const setVideoBrightness = useSettingsStore(state => state.setVideoBrightness); - const videoContrast = useSettingsStore(state => state.videoContrast); - const setVideoContrast = useSettingsStore(state => state.setVideoContrast); + const { + videoSaturation, setVideoSaturation, + videoBrightness, setVideoBrightness, + videoContrast, setVideoContrast + } = useSettingsStore(); useEffect(() => { send("getStreamQualityFactor", {}, resp => { diff --git a/ui/src/routes/devices.$id.tsx b/ui/src/routes/devices.$id.tsx index 1785bcd..b1d6250 100644 --- a/ui/src/routes/devices.$id.tsx +++ b/ui/src/routes/devices.$id.tsx @@ -18,10 +18,11 @@ import useWebSocket from "react-use-websocket"; import { cx } from "@/cva.config"; import { - HidState, KeyboardLedState, + KeysDownState, NetworkState, - UpdateState, + OtaState, + USBStates, useDeviceStore, useHidStore, useMountMediaStore, @@ -37,7 +38,7 @@ import WebRTCVideo from "@components/WebRTCVideo"; import { checkAuth, isInCloud, isOnDevice } from "@/main"; import DashboardNavbar from "@components/Header"; 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 { CLOUD_API, DEVICE_API } from "@/ui.config"; @@ -127,22 +128,22 @@ export default function KvmIdRoute() { const authMode = "authMode" in loaderResp ? loaderResp.authMode : null; const params = useParams() as { id: string }; - const sidebarView = useUiStore(state => state.sidebarView); - const [queryParams, setQueryParams] = useSearchParams(); + const { sidebarView, setSidebarView, disableVideoFocusTrap } = useUiStore(); + 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 isLegacySignalingEnabled = useRef(false); - const [connectionFailed, setConnectionFailed] = useState(false); const navigate = useNavigate(); @@ -211,7 +212,7 @@ export default function KvmIdRoute() { clearInterval(checkInterval); setLoadingMessage("Connection established"); } else if (attempts >= 10) { - console.log( + console.warn( "[setRemoteSessionDescription] Failed to establish connection after 10 attempts", { connectionState: pc.connectionState, @@ -247,27 +248,27 @@ export default function KvmIdRoute() { reconnectAttempts: 15, reconnectInterval: 1000, onReconnectStop: () => { - console.log("Reconnect stopped"); + console.debug("Reconnect stopped"); cleanupAndStopReconnecting(); }, shouldReconnect(event) { - console.log("[Websocket] shouldReconnect", event); + console.debug("[Websocket] shouldReconnect", event); // TODO: Why true? return true; }, 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 }, 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 }, onOpen() { - console.log("[Websocket] onOpen"); + console.debug("[Websocket] onOpen"); }, onMessage: message => { @@ -289,8 +290,8 @@ export default function KvmIdRoute() { const parsedMessage = JSON.parse(message.data); if (parsedMessage.type === "device-metadata") { const { deviceVersion } = parsedMessage.data; - console.log("[Websocket] Received device-metadata message"); - console.log("[Websocket] Device version", deviceVersion); + console.debug("[Websocket] Received device-metadata message"); + console.debug("[Websocket] Device version", deviceVersion); // If the device version is not set, we can assume the device is using the legacy signaling if (!deviceVersion) { console.log("[Websocket] Device is using legacy signaling"); @@ -308,7 +309,7 @@ export default function KvmIdRoute() { if (!peerConnection) return; if (parsedMessage.type === "answer") { - console.log("[Websocket] Received answer"); + console.debug("[Websocket] Received answer"); const readyForOffer = // If we're making an offer, we don't want to accept an answer !makingOffer && @@ -322,7 +323,7 @@ export default function KvmIdRoute() { // Set so we don't accept an answer while we're setting the remote description isSettingRemoteAnswerPending.current = parsedMessage.type === "answer"; - console.log( + console.debug( "[Websocket] Setting remote answer pending", isSettingRemoteAnswerPending.current, ); @@ -338,7 +339,7 @@ export default function KvmIdRoute() { // Reset the remote answer pending flag isSettingRemoteAnswerPending.current = false; } 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; peerConnection.addIceCandidate(candidate); } @@ -384,7 +385,7 @@ export default function KvmIdRoute() { return; } - console.log("Successfully got Remote Session Description. Setting."); + console.debug("Successfully got Remote Session Description. Setting."); setLoadingMessage("Setting remote session description..."); const decodedSd = atob(json.sd); @@ -395,13 +396,13 @@ export default function KvmIdRoute() { ); const setupPeerConnection = useCallback(async () => { - console.log("[setupPeerConnection] Setting up peer connection"); + console.debug("[setupPeerConnection] Setting up peer connection"); setConnectionFailed(false); setLoadingMessage("Connecting to device..."); let pc: RTCPeerConnection; try { - console.log("[setupPeerConnection] Creating peer connection"); + console.debug("[setupPeerConnection] Creating peer connection"); setLoadingMessage("Creating peer connection..."); pc = new RTCPeerConnection({ // We only use STUN or TURN servers if we're in the cloud @@ -411,7 +412,7 @@ export default function KvmIdRoute() { }); setPeerConnectionState(pc.connectionState); - console.log("[setupPeerConnection] Peer connection created", pc); + console.debug("[setupPeerConnection] Peer connection created", pc); setLoadingMessage("Setting up connection to device..."); } catch (e) { console.error(`[setupPeerConnection] Error creating peer connection: ${e}`); @@ -423,13 +424,13 @@ export default function KvmIdRoute() { // Set up event listeners and data channels pc.onconnectionstatechange = () => { - console.log("[setupPeerConnection] Connection state changed", pc.connectionState); + console.debug("[setupPeerConnection] Connection state changed", pc.connectionState); setPeerConnectionState(pc.connectionState); }; pc.onnegotiationneeded = async () => { try { - console.log("[setupPeerConnection] Creating offer"); + console.debug("[setupPeerConnection] Creating offer"); makingOffer.current = true; const offer = await pc.createOffer(); @@ -439,7 +440,7 @@ export default function KvmIdRoute() { if (isNewSignalingEnabled) { sendWebRTCSignal("offer", { sd: sd }); } else { - console.log("Legacy signanling. Waiting for ICE Gathering to complete..."); + console.log("Legacy signaling. Waiting for ICE Gathering to complete..."); } } catch (e) { console.error( @@ -461,7 +462,7 @@ export default function KvmIdRoute() { pc.onicegatheringstatechange = event => { const pc = event.currentTarget as RTCPeerConnection; if (pc.iceGatheringState === "complete") { - console.log("ICE Gathering completed"); + console.debug("ICE Gathering completed"); setLoadingMessage("ICE Gathering completed"); if (isLegacySignalingEnabled.current) { @@ -469,13 +470,13 @@ export default function KvmIdRoute() { legacyHTTPSignaling(pc); } } else if (pc.iceGatheringState === "gathering") { - console.log("ICE Gathering Started"); + console.debug("ICE Gathering Started"); setLoadingMessage("Gathering ICE candidates..."); } }; pc.ontrack = function (event) { - setMediaMediaStream(event.streams[0]); + setMediaStream(event.streams[0]); }; setTransceiver(pc.addTransceiver("video", { direction: "recvonly" })); @@ -497,7 +498,7 @@ export default function KvmIdRoute() { legacyHTTPSignaling, sendWebRTCSignal, setDiskChannel, - setMediaMediaStream, + setMediaStream, setPeerConnection, setPeerConnectionState, setRpcDataChannel, @@ -506,15 +507,13 @@ export default function KvmIdRoute() { useEffect(() => { if (peerConnectionState === "failed") { - console.log("Connection failed, closing peer connection"); + console.warn("Connection failed, closing peer connection"); cleanupAndStopReconnecting(); } }, [peerConnectionState, cleanupAndStopReconnecting]); // Cleanup effect - const clearInboundRtpStats = useRTCStore(state => state.clearInboundRtpStats); - const clearCandidatePairStats = useRTCStore(state => state.clearCandidatePairStats); - const setSidebarView = useUiStore(state => state.setSidebarView); + const { clearInboundRtpStats, clearCandidatePairStats } = useRTCStore(); useEffect(() => { return () => { @@ -545,11 +544,10 @@ export default function KvmIdRoute() { if (!lastRemoteStat?.length) return; const remoteCandidateIsUsingTurn = lastRemoteStat[1].candidateType === "relay"; // [0] is the timestamp, which we don't care about here - setIsTurnServerInUse(localCandidateIsUsingTurn || remoteCandidateIsUsingTurn); - }, [peerConnectionState, setIsTurnServerInUse]); + setTurnServerInUse(localCandidateIsUsingTurn || remoteCandidateIsUsingTurn); + }, [peerConnectionState, setTurnServerInUse]); // TURN server usage reporting - const isTurnServerInUse = useRTCStore(state => state.isTurnServerInUse); const lastBytesReceived = useRef(0); const lastBytesSent = useRef(0); @@ -582,15 +580,13 @@ export default function KvmIdRoute() { }); }, 10000); - const setNetworkState = useNetworkStateStore(state => state.setNetworkState); - - const setUsbState = useHidStore(state => state.setUsbState); - const setHdmiState = useVideoStore(state => state.setHdmiState); - - const keyboardLedState = useHidStore(state => state.keyboardLedState); - const setKeyboardLedState = useHidStore(state => state.setKeyboardLedState); - - const setKeyboardLedStateSyncAvailable = useHidStore(state => state.setKeyboardLedStateSyncAvailable); + const { setNetworkState} = useNetworkStateStore(); + const { setHdmiState } = useVideoStore(); + const { + keyboardLedState, setKeyboardLedState, + keysDownState, setKeysDownState, setUsbState, + setkeyPressReportApiAvailable + } = useHidStore(); const [hasUpdated, setHasUpdated] = useState(false); const { navigateTo } = useDeviceUiNavigation(); @@ -601,27 +597,38 @@ export default function KvmIdRoute() { } 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") { - setHdmiState(resp.params as Parameters[0]); + const hdmiState = resp.params as Parameters[0]; + console.debug("Setting HDMI state", hdmiState); + setHdmiState(hdmiState); } if (resp.method === "networkState") { - console.log("Setting network state", resp.params); + console.debug("Setting network state", resp.params); setNetworkState(resp.params as NetworkState); } if (resp.method === "keyboardLedState") { const ledState = resp.params as KeyboardLedState; - console.log("Setting keyboard led state", ledState); + console.debug("Setting keyboard led state", 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") { - const otaState = resp.params as UpdateState["otaState"]; + const otaState = resp.params as OtaState; + console.debug("Setting OTA state", otaState); setOtaState(otaState); 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(() => { if (rpcDataChannel?.readyState !== "open") return; - send("getVideoState", {}, resp => { + console.log("Requesting video state"); + send("getVideoState", {}, (resp: JsonRpcResponse) => { if ("error" in resp) return; - setHdmiState(resp.result as Parameters[0]); + const hdmiState = resp.result as Parameters[0]; + console.debug("Setting HDMI state", hdmiState); + setHdmiState(hdmiState); }); }, [rpcDataChannel?.readyState, send, setHdmiState]); + const [needLedState, setNeedLedState] = useState(true); + // request keyboard led state from the device useEffect(() => { if (rpcDataChannel?.readyState !== "open") return; - if (keyboardLedState !== undefined) return; + if (!needLedState) return; 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) { // -32601 means the method is not supported if (resp.error.code === -32601) { - setKeyboardLedStateSyncAvailable(false); - console.error("Failed to get keyboard led state, disabling sync", resp.error); + // if we don't support key down state, we know key press is also not available + console.warn("Failed to get key down state, switching to old-school", resp.error); + setkeyPressReportApiAvailable(false); } 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); - setKeyboardLedState(resp.result as KeyboardLedState); - setKeyboardLedStateSyncAvailable(true); + setNeedKeyDownState(false); }); - }, [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 useEffect(() => { @@ -686,14 +721,13 @@ export default function KvmIdRoute() { } }, [navigate, navigateTo, queryParams, setModalView, setQueryParams]); - const diskChannel = useRTCStore(state => state.diskChannel)!; - const file = useMountMediaStore(state => state.localFile)!; + const { localFile } = useMountMediaStore(); useEffect(() => { - if (!diskChannel || !file) return; + if (!diskChannel || !localFile) return; diskChannel.onmessage = async e => { - console.log("Received", e.data); + console.debug("Received", 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 header = new ArrayBuffer(16); const headerView = new DataView(header); @@ -704,11 +738,9 @@ export default function KvmIdRoute() { fullData.set(new Uint8Array(buf), header.byteLength); diskChannel.send(fullData); }; - }, [diskChannel, file]); + }, [diskChannel, localFile]); // System update - const disableVideoFocusTrap = useUiStore(state => state.disableVideoFocusTrap); - const [kvmTerminal, setKvmTerminal] = useState(null); const [serialConsole, setSerialConsole] = useState(null); @@ -728,17 +760,15 @@ export default function KvmIdRoute() { if (location.pathname !== "/other-session") navigateTo("/"); }, [navigateTo, location.pathname]); - const appVersion = useDeviceStore(state => state.appVersion); - const setAppVersion = useDeviceStore(state => state.setAppVersion); - const setSystemVersion = useDeviceStore(state => state.setSystemVersion); + const { appVersion, setAppVersion, setSystemVersion} = useDeviceStore(); useEffect(() => { if (appVersion) return; - send("getUpdateStatus", {}, async resp => { + send("getUpdateStatus", {}, async (resp: JsonRpcResponse) => { if ("error" in resp) { notifications.error(`Failed to get device version: ${resp.error}`); - return + return } const result = resp.result as SystemVersionInfo; @@ -875,7 +905,7 @@ interface SidebarContainerProps { } function SidebarContainer(props: SidebarContainerProps) { - const { sidebarView }= props; + const { sidebarView } = props; return (