diff --git a/config.go b/config.go index 3ae4066..eb707c5 100644 --- a/config.go +++ b/config.go @@ -12,25 +12,27 @@ type WakeOnLanDevice struct { } type Config struct { - CloudURL string `json:"cloud_url"` - CloudToken string `json:"cloud_token"` - GoogleIdentity string `json:"google_identity"` - JigglerEnabled bool `json:"jiggler_enabled"` - AutoUpdateEnabled bool `json:"auto_update_enabled"` - KeyboardLayout string `json:"keyboard_layout"` - IncludePreRelease bool `json:"include_pre_release"` - HashedPassword string `json:"hashed_password"` - LocalAuthToken string `json:"local_auth_token"` - LocalAuthMode string `json:"localAuthMode"` //TODO: fix it with migration - WakeOnLanDevices []WakeOnLanDevice `json:"wake_on_lan_devices"` + CloudURL string `json:"cloud_url"` + CloudToken string `json:"cloud_token"` + GoogleIdentity string `json:"google_identity"` + JigglerEnabled bool `json:"jiggler_enabled"` + AutoUpdateEnabled bool `json:"auto_update_enabled"` + KeyboardLayout string `json:"keyboard_layout"` + KeyboardMappingEnabled bool `json:"keyboard_mapping_enabled"` + IncludePreRelease bool `json:"include_pre_release"` + HashedPassword string `json:"hashed_password"` + LocalAuthToken string `json:"local_auth_token"` + LocalAuthMode string `json:"localAuthMode"` //TODO: fix it with migration + WakeOnLanDevices []WakeOnLanDevice `json:"wake_on_lan_devices"` } const configPath = "/userdata/kvm_config.json" var defaultConfig = &Config{ - CloudURL: "https://api.jetkvm.com", - AutoUpdateEnabled: true, // Set a default value - KeyboardLayout: "us", + CloudURL: "https://api.jetkvm.com", + AutoUpdateEnabled: true, // Set a default value + KeyboardLayout: "us", + KeyboardMappingEnabled: false, } var config *Config diff --git a/jsonrpc.go b/jsonrpc.go index f7543bb..c859fc5 100644 --- a/jsonrpc.go +++ b/jsonrpc.go @@ -143,6 +143,18 @@ func rpcSetKeyboardLayout(KeyboardLayout string) (string, error) { return KeyboardLayout, nil } +func rpcGetKeyboardMappingState() (bool, error) { + return config.KeyboardMappingEnabled, nil +} + +func rpcSetKeyboardMappingState(enabled bool) (bool, error) { + config.KeyboardMappingEnabled = enabled + if err := SaveConfig(); err != nil { + return config.KeyboardMappingEnabled, fmt.Errorf("failed to save config: %w", err) + } + return enabled, nil +} + var streamFactor = 1.0 func rpcGetStreamQualityFactor() (float64, error) { @@ -521,51 +533,53 @@ func rpcResetConfig() error { // TODO: replace this crap with code generator var rpcHandlers = map[string]RPCHandler{ - "ping": {Func: rpcPing}, - "getDeviceID": {Func: rpcGetDeviceID}, - "deregisterDevice": {Func: rpcDeregisterDevice}, - "getCloudState": {Func: rpcGetCloudState}, - "keyboardReport": {Func: rpcKeyboardReport, Params: []string{"modifier", "keys"}}, - "absMouseReport": {Func: rpcAbsMouseReport, Params: []string{"x", "y", "buttons"}}, - "wheelReport": {Func: rpcWheelReport, Params: []string{"wheelY"}}, - "getVideoState": {Func: rpcGetVideoState}, - "getUSBState": {Func: rpcGetUSBState}, - "unmountImage": {Func: rpcUnmountImage}, - "rpcMountBuiltInImage": {Func: rpcMountBuiltInImage, Params: []string{"filename"}}, - "setJigglerState": {Func: rpcSetJigglerState, Params: []string{"enabled"}}, - "getJigglerState": {Func: rpcGetJigglerState}, - "sendWOLMagicPacket": {Func: rpcSendWOLMagicPacket, Params: []string{"macAddress"}}, - "getKeyboardLayout": {Func: rpcGetKeyboardLayout}, - "setKeyboardLayout": {Func: rpcSetKeyboardLayout, Params: []string{"kbLayout"}}, - "getStreamQualityFactor": {Func: rpcGetStreamQualityFactor}, - "setStreamQualityFactor": {Func: rpcSetStreamQualityFactor, Params: []string{"factor"}}, - "getAutoUpdateState": {Func: rpcGetAutoUpdateState}, - "setAutoUpdateState": {Func: rpcSetAutoUpdateState, Params: []string{"enabled"}}, - "getEDID": {Func: rpcGetEDID}, - "setEDID": {Func: rpcSetEDID, Params: []string{"edid"}}, - "getDevChannelState": {Func: rpcGetDevChannelState}, - "setDevChannelState": {Func: rpcSetDevChannelState, Params: []string{"enabled"}}, - "getUpdateStatus": {Func: rpcGetUpdateStatus}, - "tryUpdate": {Func: rpcTryUpdate}, - "getDevModeState": {Func: rpcGetDevModeState}, - "setDevModeState": {Func: rpcSetDevModeState, Params: []string{"enabled"}}, - "getSSHKeyState": {Func: rpcGetSSHKeyState}, - "setSSHKeyState": {Func: rpcSetSSHKeyState, Params: []string{"sshKey"}}, - "setMassStorageMode": {Func: rpcSetMassStorageMode, Params: []string{"mode"}}, - "getMassStorageMode": {Func: rpcGetMassStorageMode}, - "isUpdatePending": {Func: rpcIsUpdatePending}, - "getUsbEmulationState": {Func: rpcGetUsbEmulationState}, - "setUsbEmulationState": {Func: rpcSetUsbEmulationState, Params: []string{"enabled"}}, - "checkMountUrl": {Func: rpcCheckMountUrl, Params: []string{"url"}}, - "getVirtualMediaState": {Func: rpcGetVirtualMediaState}, - "getStorageSpace": {Func: rpcGetStorageSpace}, - "mountWithHTTP": {Func: rpcMountWithHTTP, Params: []string{"url", "mode"}}, - "mountWithWebRTC": {Func: rpcMountWithWebRTC, Params: []string{"filename", "size", "mode"}}, - "mountWithStorage": {Func: rpcMountWithStorage, Params: []string{"filename", "mode"}}, - "listStorageFiles": {Func: rpcListStorageFiles}, - "deleteStorageFile": {Func: rpcDeleteStorageFile, Params: []string{"filename"}}, - "startStorageFileUpload": {Func: rpcStartStorageFileUpload, Params: []string{"filename", "size"}}, - "getWakeOnLanDevices": {Func: rpcGetWakeOnLanDevices}, - "setWakeOnLanDevices": {Func: rpcSetWakeOnLanDevices, Params: []string{"params"}}, - "resetConfig": {Func: rpcResetConfig}, + "ping": {Func: rpcPing}, + "getDeviceID": {Func: rpcGetDeviceID}, + "deregisterDevice": {Func: rpcDeregisterDevice}, + "getCloudState": {Func: rpcGetCloudState}, + "keyboardReport": {Func: rpcKeyboardReport, Params: []string{"modifier", "keys"}}, + "absMouseReport": {Func: rpcAbsMouseReport, Params: []string{"x", "y", "buttons"}}, + "wheelReport": {Func: rpcWheelReport, Params: []string{"wheelY"}}, + "getVideoState": {Func: rpcGetVideoState}, + "getUSBState": {Func: rpcGetUSBState}, + "unmountImage": {Func: rpcUnmountImage}, + "rpcMountBuiltInImage": {Func: rpcMountBuiltInImage, Params: []string{"filename"}}, + "setJigglerState": {Func: rpcSetJigglerState, Params: []string{"enabled"}}, + "getJigglerState": {Func: rpcGetJigglerState}, + "sendWOLMagicPacket": {Func: rpcSendWOLMagicPacket, Params: []string{"macAddress"}}, + "getKeyboardLayout": {Func: rpcGetKeyboardLayout}, + "setKeyboardLayout": {Func: rpcSetKeyboardLayout, Params: []string{"kbLayout"}}, + "setKeyboardMappingState": {Func: rpcSetKeyboardMappingState, Params: []string{"enabled"}}, + "getKeyboardMappingState": {Func: rpcGetKeyboardMappingState}, + "getStreamQualityFactor": {Func: rpcGetStreamQualityFactor}, + "setStreamQualityFactor": {Func: rpcSetStreamQualityFactor, Params: []string{"factor"}}, + "getAutoUpdateState": {Func: rpcGetAutoUpdateState}, + "setAutoUpdateState": {Func: rpcSetAutoUpdateState, Params: []string{"enabled"}}, + "getEDID": {Func: rpcGetEDID}, + "setEDID": {Func: rpcSetEDID, Params: []string{"edid"}}, + "getDevChannelState": {Func: rpcGetDevChannelState}, + "setDevChannelState": {Func: rpcSetDevChannelState, Params: []string{"enabled"}}, + "getUpdateStatus": {Func: rpcGetUpdateStatus}, + "tryUpdate": {Func: rpcTryUpdate}, + "getDevModeState": {Func: rpcGetDevModeState}, + "setDevModeState": {Func: rpcSetDevModeState, Params: []string{"enabled"}}, + "getSSHKeyState": {Func: rpcGetSSHKeyState}, + "setSSHKeyState": {Func: rpcSetSSHKeyState, Params: []string{"sshKey"}}, + "setMassStorageMode": {Func: rpcSetMassStorageMode, Params: []string{"mode"}}, + "getMassStorageMode": {Func: rpcGetMassStorageMode}, + "isUpdatePending": {Func: rpcIsUpdatePending}, + "getUsbEmulationState": {Func: rpcGetUsbEmulationState}, + "setUsbEmulationState": {Func: rpcSetUsbEmulationState, Params: []string{"enabled"}}, + "checkMountUrl": {Func: rpcCheckMountUrl, Params: []string{"url"}}, + "getVirtualMediaState": {Func: rpcGetVirtualMediaState}, + "getStorageSpace": {Func: rpcGetStorageSpace}, + "mountWithHTTP": {Func: rpcMountWithHTTP, Params: []string{"url", "mode"}}, + "mountWithWebRTC": {Func: rpcMountWithWebRTC, Params: []string{"filename", "size", "mode"}}, + "mountWithStorage": {Func: rpcMountWithStorage, Params: []string{"filename", "mode"}}, + "listStorageFiles": {Func: rpcListStorageFiles}, + "deleteStorageFile": {Func: rpcDeleteStorageFile, Params: []string{"filename"}}, + "startStorageFileUpload": {Func: rpcStartStorageFileUpload, Params: []string{"filename", "size"}}, + "getWakeOnLanDevices": {Func: rpcGetWakeOnLanDevices}, + "setWakeOnLanDevices": {Func: rpcSetWakeOnLanDevices, Params: []string{"params"}}, + "resetConfig": {Func: rpcResetConfig}, } diff --git a/ui/src/components/SelectMenuBasic.tsx b/ui/src/components/SelectMenuBasic.tsx index eb4c540..f515eb3 100644 --- a/ui/src/components/SelectMenuBasic.tsx +++ b/ui/src/components/SelectMenuBasic.tsx @@ -24,6 +24,7 @@ type SelectMenuProps = Pick< const sizes = { XS: "h-[24.5px] pl-3 pr-8 text-xs", SM: "h-[32px] pl-3 pr-8 text-[13px]", + SM_Wide: "h-[32px] pl-3 pr-8 mr-5 text-[13px]", MD: "h-[40px] pl-4 pr-10 text-sm", LG: "h-[48px] pl-4 pr-10 px-5 text-base", }; diff --git a/ui/src/components/WebRTCVideo.tsx b/ui/src/components/WebRTCVideo.tsx index c636bdc..080a21f 100644 --- a/ui/src/components/WebRTCVideo.tsx +++ b/ui/src/components/WebRTCVideo.tsx @@ -17,6 +17,10 @@ import useKeyboard from "@/hooks/useKeyboard"; import { useJsonRpc } from "@/hooks/useJsonRpc"; import { ConnectionErrorOverlay, HDMIErrorOverlay, LoadingOverlay } from "./VideoOverlay"; +// TODO Implement keyboard lock API to resolve #127 +// https://developer.chrome.com/docs/capabilities/web-apis/keyboard-lock +// An appropriate error message will need to be displayed in order to alert users to browser compatibility issues. + export default function WebRTCVideo() { const [keys, setKeys] = useState(useKeyboardMappingsStore.keys); const [chars, setChars] = useState(useKeyboardMappingsStore.chars); @@ -155,8 +159,6 @@ export default function WebRTCVideo() { // Invert the scroll value to match expected behavior const invertedScroll = -roundedScroll; - // TODO remove debug logs - console.log("wheelReport", { wheelY: invertedScroll }); send("wheelReport", { wheelY: invertedScroll }); // TODO this is making scrolling feel slow and sluggish, also throwing a violation in chrome @@ -179,7 +181,7 @@ export default function WebRTCVideo() { // TODO remove debug logging console.log(shiftKey + " " +ctrlKey + " " +altKey + " " +metaKey + " " +mappedKeyModifers.shift + " "+mappedKeyModifers.altLeft + " "+mappedKeyModifers.altRight + " ") - const filteredModifiers = activeModifiers.filter(Boolean);3 + const filteredModifiers = activeModifiers.filter(Boolean); // Example: activeModifiers = [0x01, 0x02, 0x04, 0x08] // Assuming 0x01 = ControlLeft, 0x02 = ShiftLeft, 0x04 = AltLeft, 0x08 = MetaLeft return ( @@ -210,8 +212,13 @@ export default function WebRTCVideo() { modifier => altKey || mappedKeyModifers.altLeft || + (modifier !== modifiers["AltLeft"]), + ) + .filter( + modifier => + altKey || mappedKeyModifers.altRight || - (modifier !== modifiers["AltLeft"] && modifier !== modifiers["AltRight"]), + (modifier !== modifiers["AltRight"]) ) // Meta: Keep if Meta is pressed or if the key isn't a Meta key // Example: If metaKey is true, keep all modifiers @@ -230,8 +237,9 @@ export default function WebRTCVideo() { async (e: KeyboardEvent) => { e.preventDefault(); const prev = useHidStore.getState(); - let code = e.code; - const localisedKey = e.key; + const code = e.code; + console.log("MAPPING ENABLED: " + settings.keyboardMappingEnabled) + var localisedKey = settings.keyboardMappingEnabled ? e.key : code; console.log(e); console.log("Localised Key: " + localisedKey); @@ -282,12 +290,12 @@ export default function WebRTCVideo() { // 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 - // TODO add this to the activekey state - // TODO set this to remove from activekeystate as well if (e.metaKey) { setTimeout(() => { const prev = useHidStore.getState(); sendKeyboardEvent([], newModifiers || prev.activeModifiers); + activeKeyState.current.delete("MetaLeft"); + activeKeyState.current.delete("MetaRight"); }, 10); } @@ -302,6 +310,7 @@ export default function WebRTCVideo() { chars, keys, modifiers, + settings, ], ); diff --git a/ui/src/components/popovers/PasteModal.tsx b/ui/src/components/popovers/PasteModal.tsx index 9728cb8..66c2694 100644 --- a/ui/src/components/popovers/PasteModal.tsx +++ b/ui/src/components/popovers/PasteModal.tsx @@ -79,7 +79,7 @@ export default function PasteModal() { } catch (error) { notifications.error("Failed to paste text"); } - }, [rpcDataChannel?.readyState, send, setDisableVideoFocusTrap, setPasteMode]); + }, [rpcDataChannel?.readyState, send, setDisableVideoFocusTrap, setPasteMode, chars, keys, modifiers]); useEffect(() => { if (TextAreaRef.current) { @@ -144,6 +144,9 @@ export default function PasteModal() { The following characters won't be pasted as the current keyboard layout does not contain a valid mapping:{" "} {invalidChars.join(", ")} + + Tip: You can set your desired keyboard layout in settings, and remember to enable keyboard mapping. + )} diff --git a/ui/src/components/sidebar/settings.tsx b/ui/src/components/sidebar/settings.tsx index a6c3fe1..4e8d37c 100644 --- a/ui/src/components/sidebar/settings.tsx +++ b/ui/src/components/sidebar/settings.tsx @@ -79,6 +79,7 @@ export default function SettingsSidebar() { const settings = useSettingsStore(); const [send] = useJsonRpc(); const [keyboardLayout, setKeyboardLayout] = useState("us"); + const [kbMappingEnabled, setKeyboardMapping] = useState(false); const [streamQuality, setStreamQuality] = useState("1"); const [autoUpdate, setAutoUpdate] = useState(true); const [devChannel, setDevChannel] = useState(false); @@ -161,6 +162,20 @@ export default function SettingsSidebar() { }); }; + const handleKeyboardMappingChange = (enabled: boolean) => { + send("setKeyboardMappingState", { enabled }, resp => { + if ("error" in resp) { + notifications.error( + `Failed to set keyboard maping state state: ${resp.error.data || "Unknown error"}`, + ); + return; + } + settings.setkeyboardMappingEnabled(enabled); + useKeyboardMappingsStore.setMappingsState(enabled); + setKeyboardMapping(enabled); + }); + }; + const handleStreamQualityChange = (factor: string) => { send("setStreamQualityFactor", { factor: Number(factor) }, resp => { if ("error" in resp) { @@ -295,6 +310,13 @@ export default function SettingsSidebar() { useKeyboardMappingsStore.setLayout(String(resp.result)) }); + send("getKeyboardMappingState", {}, resp => { + if ("error" in resp) return; + setKeyboardMapping(resp.result as boolean); + settings.setkeyboardMappingEnabled(resp.result as boolean); + useKeyboardMappingsStore.setMappingsState(resp.result as boolean); + }); + send("getStreamQualityFactor", {}, resp => { if ("error" in resp) return; setStreamQuality(String(resp.result)); @@ -536,20 +558,32 @@ export default function SettingsSidebar() { description="Customize keyboard behaviour" />