Compare commits

...

7 Commits

Author SHA1 Message Date
Marc Brooks 443aff5152
Merge 84e4b44df0 into 584768bacf 2025-07-10 12:23:30 +02:00
Aveline 584768bacf
chore: remove /device/ui-config.js endpoint (#678) 2025-07-10 12:04:47 +02:00
adammkelly 488276f3a8
feat(ui): reboot device (#421) (#505) 2025-07-10 00:02:13 +02:00
Patrick Hofmann 7267347261
feat(dc-power-extension): power restore mode in DCPowerControl component (#672)
* DC-extension: Supporting to set the power restore mode in DCPowerControl component

* fixing lint issue
2025-07-09 23:58:46 +02:00
Marc Brooks 393bc122d4
chore: fix the base usb configuration (#610)
In reviewing the config.go settings for idProduct and bcdDevice are not formatted correctly. All examples on GitHub have 0x0104 and 0x0100 respectively. The idProduct value gets overwritten with valid values when you change the configuration (because they are correct in the options), but until you do the USB initialization will not be correct.
2025-07-09 23:57:51 +02:00
Marc Brooks 6d13e1be12
chore: remove ActionBar-Ctrl-Alt-Del (#669) 2025-07-09 23:53:44 +02:00
Marc Brooks 84e4b44df0
Treats all modifiers as "sticky" keys for use with just touch/mouse
Made the operation keys look more like a keyboard
Add persistent state management for the Shift/Ctrl/Alt/Meta
Add infoBar indicators for Shift/Ctr/Alt/Meta/AltGr
Remove redundant Break it's actually on keyboard as (Pause)
Add a style for the virtual keyboard "depressed" buttons.
Delete the no-longer-needed button variants
Added missing keycodes
Added modifier tracking for all the other keys
Ensures the InfoBar tracks for physical and virtual keyboard
Shows what buttons are depressed for sticky keys
Now treats all the Shift/Control/Alt/Meta/AltGr keys as if they were sticky keys so users can click the button and hit the next key,
2025-06-17 11:19:00 -05:00
17 changed files with 509 additions and 209 deletions

View File

@ -30,8 +30,8 @@ var defaultGadgetConfig = map[string]gadgetConfigItem{
attrs: gadgetAttributes{ attrs: gadgetAttributes{
"bcdUSB": "0x0200", // USB 2.0 "bcdUSB": "0x0200", // USB 2.0
"idVendor": "0x1d6b", // The Linux Foundation "idVendor": "0x1d6b", // The Linux Foundation
"idProduct": "0104", // Multifunction Composite Gadget "idProduct": "0x0104", // Multifunction Composite Gadget
"bcdDevice": "0100", "bcdDevice": "0x0100", // USB2
}, },
configAttrs: gadgetAttributes{ configAttrs: gadgetAttributes{
"MaxPower": "250", // in unit of 2mA "MaxPower": "250", // in unit of 2mA

View File

@ -681,10 +681,11 @@ func rpcResetConfig() error {
} }
type DCPowerState struct { type DCPowerState struct {
IsOn bool `json:"isOn"` IsOn bool `json:"isOn"`
Voltage float64 `json:"voltage"` Voltage float64 `json:"voltage"`
Current float64 `json:"current"` Current float64 `json:"current"`
Power float64 `json:"power"` Power float64 `json:"power"`
RestoreState int `json:"restoreState"`
} }
func rpcGetDCPowerState() (DCPowerState, error) { func rpcGetDCPowerState() (DCPowerState, error) {
@ -700,6 +701,15 @@ func rpcSetDCPowerState(enabled bool) error {
return nil return nil
} }
func rpcSetDCRestoreState(state int) error {
logger.Info().Int("state", state).Msg("Setting DC restore state")
err := setDCRestoreState(state)
if err != nil {
return fmt.Errorf("failed to set DC restore state: %w", err)
}
return nil
}
func rpcGetActiveExtension() (string, error) { func rpcGetActiveExtension() (string, error) {
return config.ActiveExtension, nil return config.ActiveExtension, nil
} }
@ -1088,6 +1098,7 @@ var rpcHandlers = map[string]RPCHandler{
"getBacklightSettings": {Func: rpcGetBacklightSettings}, "getBacklightSettings": {Func: rpcGetBacklightSettings},
"getDCPowerState": {Func: rpcGetDCPowerState}, "getDCPowerState": {Func: rpcGetDCPowerState},
"setDCPowerState": {Func: rpcSetDCPowerState, Params: []string{"enabled"}}, "setDCPowerState": {Func: rpcSetDCPowerState, Params: []string{"enabled"}},
"setDCRestoreState": {Func: rpcSetDCRestoreState, Params: []string{"state"}},
"getActiveExtension": {Func: rpcGetActiveExtension}, "getActiveExtension": {Func: rpcGetActiveExtension},
"setActiveExtension": {Func: rpcSetActiveExtension, Params: []string{"extensionId"}}, "setActiveExtension": {Func: rpcSetActiveExtension, Params: []string{"extensionId"}},
"getATXState": {Func: rpcGetATXState}, "getATXState": {Func: rpcGetATXState},

View File

@ -142,6 +142,7 @@ var dcState DCPowerState
func runDCControl() { func runDCControl() {
scopedLogger := serialLogger.With().Str("service", "dc_control").Logger() scopedLogger := serialLogger.With().Str("service", "dc_control").Logger()
reader := bufio.NewReader(port) reader := bufio.NewReader(port)
hasRestoreFeature := false
for { for {
line, err := reader.ReadString('\n') line, err := reader.ReadString('\n')
if err != nil { if err != nil {
@ -151,7 +152,13 @@ func runDCControl() {
// Split the line by semicolon // Split the line by semicolon
parts := strings.Split(strings.TrimSpace(line), ";") parts := strings.Split(strings.TrimSpace(line), ";")
if len(parts) != 4 { if len(parts) == 5 {
scopedLogger.Debug().Str("line", line).Msg("Detected DC extension with restore feature")
hasRestoreFeature = true
} else if len(parts) == 4 {
scopedLogger.Debug().Str("line", line).Msg("Detected DC extension without restore feature")
hasRestoreFeature = false
} else {
scopedLogger.Warn().Str("line", line).Msg("Invalid line") scopedLogger.Warn().Str("line", line).Msg("Invalid line")
continue continue
} }
@ -163,6 +170,17 @@ func runDCControl() {
continue continue
} }
dcState.IsOn = powerState == 1 dcState.IsOn = powerState == 1
if hasRestoreFeature {
restoreState, err := strconv.Atoi(parts[4])
if err != nil {
scopedLogger.Warn().Err(err).Msg("Invalid restore state")
continue
}
dcState.RestoreState = restoreState
} else {
// -1 means not supported
dcState.RestoreState = -1
}
milliVolts, err := strconv.ParseFloat(parts[1], 64) milliVolts, err := strconv.ParseFloat(parts[1], 64)
if err != nil { if err != nil {
scopedLogger.Warn().Err(err).Msg("Invalid voltage") scopedLogger.Warn().Err(err).Msg("Invalid voltage")
@ -210,6 +228,25 @@ func setDCPowerState(on bool) error {
return nil return nil
} }
func setDCRestoreState(state int) error {
_, err := port.Write([]byte("\n"))
if err != nil {
return err
}
command := "RESTORE_MODE_OFF\n"
switch state {
case 1:
command = "RESTORE_MODE_ON\n"
case 2:
command = "RESTORE_MODE_LAST_STATE\n"
}
_, err = port.Write([]byte(command))
if err != nil {
return err
}
return nil
}
var defaultMode = &serial.Mode{ var defaultMode = &serial.Mode{
BaudRate: 115200, BaudRate: 115200,
DataBits: 8, DataBits: 8,

View File

@ -262,23 +262,6 @@ export default function Actionbar({
}} }}
/> />
</div> </div>
{/* {useSettingsStore().actionBarCtrlAltDel && (
<div className="hidden lg:block">
<Button
size="XS"
theme="light"
text="Ctrl + Alt + Del"
LeadingIcon={FaLock}
onClick={() => {
sendKeyboardEvent(
[keys["Delete"]],
[modifiers["ControlLeft"], modifiers["AltLeft"]],
);
setTimeout(resetKeyboardState, 100);
}}
/>
</div>
)} */}
<div> <div>
<Button <Button
size="XS" size="XS"

View File

@ -41,6 +41,12 @@ export default function InfoBar() {
const keyboardLedStateSyncAvailable = useHidStore(state => state.keyboardLedStateSyncAvailable); const keyboardLedStateSyncAvailable = useHidStore(state => state.keyboardLedStateSyncAvailable);
const keyboardLedSync = useSettingsStore(state => state.keyboardLedSync); const keyboardLedSync = useSettingsStore(state => state.keyboardLedSync);
const isShiftActive = useHidStore(state => state.isShiftActive);
const isCtrlActive = useHidStore(state => state.isCtrlActive);
const isAltActive = useHidStore(state => state.isAltActive);
const isMetaActive = useHidStore(state => state.isMetaActive);
const isAltGrActive = useHidStore(state => state.isAltGrActive);
const isTurnServerInUse = useRTCStore(state => state.isTurnServerInUse); const isTurnServerInUse = useRTCStore(state => state.isTurnServerInUse);
const usbState = useHidStore(state => state.usbState); const usbState = useHidStore(state => state.usbState);
@ -135,6 +141,56 @@ export default function InfoBar() {
{keyboardLedSync === "browser" ? "Browser" : "Host"} {keyboardLedSync === "browser" ? "Browser" : "Host"}
</div> </div>
) : null} ) : null}
<div
className={cx(
"shrink-0 p-1 px-1.5 text-xs",
isShiftActive
? "text-black dark:text-white"
: "text-slate-800/20 dark:text-slate-300/20",
)}
>
Shift
</div>
<div
className={cx(
"shrink-0 p-1 px-1.5 text-xs",
isCtrlActive
? "text-black dark:text-white"
: "text-slate-800/20 dark:text-slate-300/20",
)}
>
Ctrl
</div>
<div
className={cx(
"shrink-0 p-1 px-1.5 text-xs",
isAltActive
? "text-black dark:text-white"
: "text-slate-800/20 dark:text-slate-300/20",
)}
>
Alt
</div>
<div
className={cx(
"shrink-0 p-1 px-1.5 text-xs",
isMetaActive
? "text-black dark:text-white"
: "text-slate-800/20 dark:text-slate-300/20",
)}
>
Meta
</div>
<div
className={cx(
"shrink-0 p-1 px-1.5 text-xs",
isAltGrActive
? "text-black dark:text-white"
: "text-slate-800/20 dark:text-slate-300/20",
)}
>
AltGr
</div>
<div <div
className={cx( className={cx(
"shrink-0 p-1 px-1.5 text-xs", "shrink-0 p-1 px-1.5 text-xs",

View File

@ -27,6 +27,7 @@ const AttachIcon = ({ className }: { className?: string }) => {
function KeyboardWrapper() { function KeyboardWrapper() {
const [layoutName, setLayoutName] = useState("default"); const [layoutName, setLayoutName] = useState("default");
const [depressedButtons, setDepressedButtons] = useState("");
const keyboardRef = useRef<HTMLDivElement>(null); const keyboardRef = useRef<HTMLDivElement>(null);
const showAttachedVirtualKeyboard = useUiStore( const showAttachedVirtualKeyboard = useUiStore(
@ -54,6 +55,21 @@ function KeyboardWrapper() {
const setIsCapsLockActive = useHidStore(state => state.setIsCapsLockActive); const setIsCapsLockActive = useHidStore(state => state.setIsCapsLockActive);
const isShiftActive = useHidStore(state => state.isShiftActive);
const setIsShiftActive = useHidStore(state => state.setIsShiftActive);
const isCtrlActive = useHidStore(state => state.isCtrlActive);
const setIsCtrlActive = useHidStore(state => state.setIsCtrlActive);
const isAltActive = useHidStore(state => state.isAltActive);
const setIsAltActive = useHidStore(state => state.setIsAltActive);
const isMetaActive = useHidStore(state => state.isMetaActive);
const setIsMetaActive = useHidStore(state => state.setIsMetaActive);
const isAltGrActive = useHidStore(state => state.isAltGrActive);
const setIsAltGrActive = useHidStore(state => state.setIsAltGrActive);
const startDrag = useCallback((e: MouseEvent | TouchEvent) => { const startDrag = useCallback((e: MouseEvent | TouchEvent) => {
if (!keyboardRef.current) return; if (!keyboardRef.current) return;
if (e instanceof TouchEvent && e.touches.length > 1) return; if (e instanceof TouchEvent && e.touches.length > 1) return;
@ -123,80 +139,123 @@ function KeyboardWrapper() {
}; };
}, [endDrag, onDrag, startDrag]); }, [endDrag, onDrag, startDrag]);
const onKeyDown = useCallback( useEffect(() => {
(key: string) => { // if you have the CapsLock "down", then the shift state is inverted
const isKeyShift = key === "{shift}" || key === "ShiftLeft" || key === "ShiftRight"; const effectiveShift = isCapsLockActive ? false === isShiftActive : isShiftActive;
const isKeyCaps = key === "CapsLock"; setLayoutName(effectiveShift ? "shift" : "default");
const cleanKey = key.replace(/[()]/g, ""); },
const keyHasShiftModifier = key.includes("("); [setLayoutName, isCapsLockActive, isShiftActive]
);
// Handle toggle of layout for shift or caps lock // this causes the buttons to look depressed/clicked depending on the sticky state
const toggleLayout = () => { useEffect(() => {
setLayoutName(prevLayout => (prevLayout === "default" ? "shift" : "default")); let buttons = "None "; // make sure we name at least one (fake) button
}; if (isCapsLockActive) buttons += "CapsLock ";
if (isShiftActive) buttons += "ShiftLeft ShiftRight ";
if (isCtrlActive) buttons += "ControlLeft ControlRight ";
if (isAltActive) buttons += "AltLeft AltRight ";
if (isMetaActive) buttons += "MetaLeft MetaRight ";
setDepressedButtons(buttons.trimEnd());
},
[setDepressedButtons, isCapsLockActive, isShiftActive, isCtrlActive, isAltActive, isMetaActive, isAltGrActive]
);
if (key === "CtrlAltDelete") { const onKeyPress = useCallback((key: string) => {
sendKeyboardEvent( // handle the fake combo keys first
[keys["Delete"]], if (key === "CtrlAltDelete") {
[modifiers["ControlLeft"], modifiers["AltLeft"]], sendKeyboardEvent(
); [keys["Delete"]],
setTimeout(resetKeyboardState, 100); [modifiers["ControlLeft"], modifiers["AltLeft"]],
return; );
} setTimeout(resetKeyboardState, 100);
return;
}
if (key === "AltMetaEscape") { if (key === "AltMetaEscape") {
sendKeyboardEvent( sendKeyboardEvent(
[keys["Escape"]], [keys["Escape"]],
[modifiers["MetaLeft"], modifiers["AltLeft"]], [modifiers["MetaLeft"], modifiers["AltLeft"]],
); );
setTimeout(resetKeyboardState, 100);
return;
}
if (key === "CtrlAltBackspace") {
sendKeyboardEvent(
[keys["Backspace"]],
[modifiers["ControlLeft"], modifiers["AltLeft"]],
);
setTimeout(resetKeyboardState, 100);
return;
}
if (isKeyShift || isKeyCaps) {
toggleLayout();
if (isCapsLockActive) {
if (!isKeyboardLedManagedByHost) {
setIsCapsLockActive(false);
}
sendKeyboardEvent([keys["CapsLock"]], []);
return;
}
}
// Handle caps lock state change
if (isKeyCaps && !isKeyboardLedManagedByHost) {
setIsCapsLockActive(!isCapsLockActive);
}
// Collect new active keys and modifiers
const newKeys = keys[cleanKey] ? [keys[cleanKey]] : [];
const newModifiers =
keyHasShiftModifier && !isCapsLockActive ? [modifiers["ShiftLeft"]] : [];
// Update current keys and modifiers
sendKeyboardEvent(newKeys, newModifiers);
// If shift was used as a modifier and caps lock is not active, revert to default layout
if (keyHasShiftModifier && !isCapsLockActive) {
setLayoutName("default");
}
setTimeout(resetKeyboardState, 100); setTimeout(resetKeyboardState, 100);
}, return;
[isCapsLockActive, isKeyboardLedManagedByHost, sendKeyboardEvent, resetKeyboardState, setIsCapsLockActive], }
if (key === "CtrlAltBackspace") {
sendKeyboardEvent(
[keys["Backspace"]],
[modifiers["ControlLeft"], modifiers["AltLeft"]],
);
setTimeout(resetKeyboardState, 100);
return;
}
// strip away the parens for shifted characters
const cleanKey = key.replace(/[()]/g, "");
const passthrough = ["PrintScreen", "SystemRequest", "Pause", "Break", "ScrollLock", "Enter", "Space"].find((value) => value === cleanKey);
if (passthrough) {
emitkeycode(cleanKey);
return;
}
// adjust the sticky state of the Shift/Ctrl/Alt/Meta/AltGr
if (key === "CapsLock" && !isKeyboardLedManagedByHost)
setIsCapsLockActive(!isCapsLockActive);
else if (key === "ShiftLeft" || key === "ShiftRight")
setIsShiftActive(!isShiftActive);
else if (key === "ControlLeft" || key === "ControlRight")
setIsCtrlActive(!isCtrlActive);
else if (key === "AltLeft" || key === "AltRight")
setIsAltActive(!isAltActive);
else if (key === "MetaLeft" || key === "MetaRight")
setIsMetaActive(!isMetaActive);
else if (key === "AltGr")
setIsAltGrActive(!isAltGrActive);
emitkeycode(cleanKey);
function emitkeycode(key: string) {
const effectiveMods: number[] = [];
if (isShiftActive)
effectiveMods.push(modifiers["ShiftLeft"]);
if (isCtrlActive)
effectiveMods.push(modifiers["ControlLeft"]);
if (isAltActive)
effectiveMods.push(modifiers["AltLeft"]);
if (isMetaActive)
effectiveMods.push(modifiers["MetaLeft"]);
if (isAltGrActive) {
effectiveMods.push(modifiers["MetaRight"]);
effectiveMods.push(modifiers["CtrlLeft"]);
}
const keycode = keys[key];
if (keycode) {
// send the keycode with modifiers
sendKeyboardEvent([keycode], effectiveMods);
}
// release the key (if one pressed), but retain the modifiers
setTimeout(() => sendKeyboardEvent([], effectiveMods), 50);
}
},
[isKeyboardLedManagedByHost,
setIsCapsLockActive, isCapsLockActive,
setIsShiftActive, isShiftActive,
setIsCtrlActive, isCtrlActive,
setIsAltActive, isAltActive,
setIsMetaActive, isMetaActive,
setIsAltGrActive, isAltGrActive,
sendKeyboardEvent, resetKeyboardState
],
); );
const virtualKeyboard = useHidStore(state => state.isVirtualKeyboardEnabled); const virtualKeyboard = useHidStore(state => state.isVirtualKeyboardEnabled);
@ -276,12 +335,16 @@ function KeyboardWrapper() {
<Keyboard <Keyboard
baseClass="simple-keyboard-main" baseClass="simple-keyboard-main"
layoutName={layoutName} layoutName={layoutName}
onKeyPress={onKeyDown} onKeyPress={onKeyPress}
buttonTheme={[ buttonTheme={[
{ {
class: "combination-key", class: "combination-key",
buttons: "CtrlAltDelete AltMetaEscape CtrlAltBackspace", buttons: "CtrlAltDelete AltMetaEscape CtrlAltBackspace",
}, },
{
class: "depressed-key",
buttons: depressedButtons
},
]} ]}
display={keyDisplayMap} display={keyDisplayMap}
layout={{ layout={{
@ -305,8 +368,6 @@ function KeyboardWrapper() {
], ],
}} }}
disableButtonHold={true} disableButtonHold={true}
syncInstanceInputs={true}
debug={false}
/> />
<div className="controlArrows"> <div className="controlArrows">
@ -314,25 +375,24 @@ function KeyboardWrapper() {
baseClass="simple-keyboard-control" baseClass="simple-keyboard-control"
theme="simple-keyboard hg-theme-default hg-layout-default" theme="simple-keyboard hg-theme-default hg-layout-default"
layoutName={layoutName} layoutName={layoutName}
onKeyPress={onKeyDown} onKeyPress={onKeyPress}
display={keyDisplayMap} display={keyDisplayMap}
layout={{ layout={{
default: ["PrintScreen ScrollLock Pause", "Insert Home Pageup", "Delete End Pagedown"], default: ["PrintScreen ScrollLock Pause", "Insert Home PageUp", "Delete End PageDown"],
shift: ["(PrintScreen) ScrollLock (Pause)", "Insert Home Pageup", "Delete End Pagedown"], shift: ["(PrintScreen) ScrollLock (Pause)", "Insert Home PageUp", "Delete End PageDown"],
}} }}
syncInstanceInputs={true} disableButtonHold={true}
debug={false}
/> />
<Keyboard <Keyboard
baseClass="simple-keyboard-arrows" baseClass="simple-keyboard-arrows"
theme="simple-keyboard hg-theme-default hg-layout-default" theme="simple-keyboard hg-theme-default hg-layout-default"
onKeyPress={onKeyDown} onKeyPress={onKeyPress}
display={keyDisplayMap} display={keyDisplayMap}
layout={{ layout={{
default: ["ArrowUp", "ArrowLeft ArrowDown ArrowRight"], default: ["ArrowUp", "ArrowLeft ArrowDown ArrowRight"],
shift: ["ArrowUp", "ArrowLeft ArrowDown ArrowRight"],
}} }}
syncInstanceInputs={true} disableButtonHold={true}
debug={false}
/> />
</div> </div>
</div> </div>

View File

@ -71,6 +71,15 @@ export default function WebRTCVideo() {
const hdmiError = ["no_lock", "no_signal", "out_of_range"].includes(hdmiState); const hdmiError = ["no_lock", "no_signal", "out_of_range"].includes(hdmiState);
const isVideoLoading = !isPlaying; const isVideoLoading = !isPlaying;
// Keyboard related states
const {
setIsShiftActive,
setIsCtrlActive,
setIsAltActive,
setIsMetaActive,
setIsAltGrActive
} = useHidStore();
const [blockWheelEvent, setBlockWheelEvent] = useState(false); const [blockWheelEvent, setBlockWheelEvent] = useState(false);
// Misc states and hooks // Misc states and hooks
@ -423,6 +432,12 @@ export default function WebRTCVideo() {
setIsScrollLockActive(e.getModifierState("ScrollLock")); setIsScrollLockActive(e.getModifierState("ScrollLock"));
} }
setIsShiftActive(e.getModifierState("Shift"))
setIsCtrlActive(e.getModifierState("Control"))
setIsAltActive(e.getModifierState("Alt"))
setIsMetaActive(e.getModifierState("Meta"))
setIsAltGrActive(e.getModifierState("AltGraph"))
if (code == "IntlBackslash" && ["`", "~"].includes(key)) { if (code == "IntlBackslash" && ["`", "~"].includes(key)) {
code = "Backquote"; code = "Backquote";
} else if (code == "Backquote" && ["§", "±"].includes(key)) { } else if (code == "Backquote" && ["§", "±"].includes(key)) {
@ -452,12 +467,17 @@ export default function WebRTCVideo() {
sendKeyboardEvent([...new Set(newKeys)], [...new Set(newModifiers)]); sendKeyboardEvent([...new Set(newKeys)], [...new Set(newModifiers)]);
}, },
[ [
handleModifierKeys,
sendKeyboardEvent,
isKeyboardLedManagedByHost, isKeyboardLedManagedByHost,
setIsNumLockActive, setIsNumLockActive,
setIsCapsLockActive, setIsCapsLockActive,
setIsScrollLockActive, setIsScrollLockActive,
setIsShiftActive,
setIsCtrlActive,
setIsAltActive,
setIsMetaActive,
setIsAltGrActive,
handleModifierKeys,
sendKeyboardEvent
], ],
); );
@ -472,6 +492,12 @@ export default function WebRTCVideo() {
setIsScrollLockActive(e.getModifierState("ScrollLock")); setIsScrollLockActive(e.getModifierState("ScrollLock"));
} }
setIsShiftActive(e.getModifierState("Shift"))
setIsCtrlActive(e.getModifierState("Control"))
setIsAltActive(e.getModifierState("Alt"))
setIsMetaActive(e.getModifierState("Meta"))
setIsAltGrActive(e.getModifierState("AltGraph"))
// Filtering out the key that was just released (keys[e.code]) // Filtering out the key that was just released (keys[e.code])
const newKeys = prev.activeKeys.filter(k => k !== keys[e.code]).filter(Boolean); const newKeys = prev.activeKeys.filter(k => k !== keys[e.code]).filter(Boolean);
@ -484,12 +510,17 @@ export default function WebRTCVideo() {
sendKeyboardEvent([...new Set(newKeys)], [...new Set(newModifiers)]); sendKeyboardEvent([...new Set(newKeys)], [...new Set(newModifiers)]);
}, },
[ [
handleModifierKeys,
sendKeyboardEvent,
isKeyboardLedManagedByHost, isKeyboardLedManagedByHost,
setIsNumLockActive, setIsNumLockActive,
setIsCapsLockActive, setIsCapsLockActive,
setIsScrollLockActive, setIsScrollLockActive,
setIsShiftActive,
setIsCtrlActive,
setIsAltActive,
setIsMetaActive,
setIsAltGrActive,
handleModifierKeys,
sendKeyboardEvent
], ],
); );

View File

@ -8,12 +8,14 @@ import { useJsonRpc } from "@/hooks/useJsonRpc";
import notifications from "@/notifications"; import notifications from "@/notifications";
import FieldLabel from "@components/FieldLabel"; import FieldLabel from "@components/FieldLabel";
import LoadingSpinner from "@components/LoadingSpinner"; import LoadingSpinner from "@components/LoadingSpinner";
import {SelectMenuBasic} from "@components/SelectMenuBasic";
interface DCPowerState { interface DCPowerState {
isOn: boolean; isOn: boolean;
voltage: number; voltage: number;
current: number; current: number;
power: number; power: number;
restoreState: number;
} }
export function DCPowerControl() { export function DCPowerControl() {
@ -43,6 +45,20 @@ export function DCPowerControl() {
getDCPowerState(); // Refresh state after change getDCPowerState(); // Refresh state after change
}); });
}; };
const handleRestoreChange = (state: number) => {
// const state = powerState?.restoreState === 0 ? 1 : powerState?.restoreState === 1 ? 2 : 0;
send("setDCRestoreState", { state }, resp => {
if ("error" in resp) {
notifications.error(
`Failed to set DC power state: ${resp.error.data || "Unknown error"}`,
);
return;
}
getDCPowerState(); // Refresh state after change
});
};
useEffect(() => { useEffect(() => {
getDCPowerState(); getDCPowerState();
@ -63,7 +79,7 @@ export function DCPowerControl() {
<LoadingSpinner className="h-6 w-6 text-blue-500 dark:text-blue-400" /> <LoadingSpinner className="h-6 w-6 text-blue-500 dark:text-blue-400" />
</Card> </Card>
) : ( ) : (
<Card className="h-[160px] animate-fadeIn opacity-0"> <Card className="animate-fadeIn opacity-0">
<div className="space-y-4 p-3"> <div className="space-y-4 p-3">
{/* Power Controls */} {/* Power Controls */}
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
@ -84,6 +100,21 @@ export function DCPowerControl() {
onClick={() => handlePowerToggle(false)} onClick={() => handlePowerToggle(false)}
/> />
</div> </div>
{powerState.restoreState > -1 ? (
<div className="flex items-center">
<SelectMenuBasic
size="SM"
label="Restore Power Loss"
value={powerState.restoreState}
onChange={e => handleRestoreChange(parseInt(e.target.value))}
options={[
{ value: '0', label: "Power OFF" },
{ value: '1', label: "Power ON" },
{ value: '2', label: "Last State" },
]}
/>
</div>
) : null}
<hr className="border-slate-700/30 dark:border-slate-600/30" /> <hr className="border-slate-700/30 dark:border-slate-600/30" />
{/* Status Display */} {/* Status Display */}

View File

@ -308,9 +308,6 @@ interface SettingsState {
keyboardLayout: string; keyboardLayout: string;
setKeyboardLayout: (layout: string) => void; setKeyboardLayout: (layout: string) => void;
actionBarCtrlAltDel: boolean;
setActionBarCtrlAltDel: (enabled: boolean) => void;
keyboardLedSync: KeyboardLedSync; keyboardLedSync: KeyboardLedSync;
setKeyboardLedSync: (sync: KeyboardLedSync) => void; setKeyboardLedSync: (sync: KeyboardLedSync) => void;
@ -359,9 +356,6 @@ export const useSettingsStore = create(
keyboardLayout: "en-US", keyboardLayout: "en-US",
setKeyboardLayout: layout => set({ keyboardLayout: layout }), setKeyboardLayout: layout => set({ keyboardLayout: layout }),
actionBarCtrlAltDel: false,
setActionBarCtrlAltDel: enabled => set({ actionBarCtrlAltDel: enabled }),
keyboardLedSync: "auto", keyboardLedSync: "auto",
setKeyboardLedSync: sync => set({ keyboardLedSync: sync }), setKeyboardLedSync: sync => set({ keyboardLedSync: sync }),
@ -481,6 +475,21 @@ export interface HidState {
isVirtualKeyboardEnabled: boolean; isVirtualKeyboardEnabled: boolean;
setVirtualKeyboardEnabled: (enabled: boolean) => void; setVirtualKeyboardEnabled: (enabled: boolean) => void;
isShiftActive: boolean;
setIsShiftActive: (enabled: boolean) => void;
isCtrlActive: boolean;
setIsCtrlActive: (enabled: boolean) => void;
isAltActive: boolean;
setIsAltActive: (enabled: boolean) => void;
isMetaActive: boolean;
setIsMetaActive: (enabled: boolean) => void;
isAltGrActive: boolean;
setIsAltGrActive: (enabled: boolean) => void;
isPasteModeEnabled: boolean; isPasteModeEnabled: boolean;
setPasteModeEnabled: (enabled: boolean) => void; setPasteModeEnabled: (enabled: boolean) => void;
@ -527,6 +536,21 @@ export const useHidStore = create<HidState>((set, get) => ({
isVirtualKeyboardEnabled: false, isVirtualKeyboardEnabled: false,
setVirtualKeyboardEnabled: enabled => set({ isVirtualKeyboardEnabled: enabled }), setVirtualKeyboardEnabled: enabled => set({ isVirtualKeyboardEnabled: enabled }),
isShiftActive: false,
setIsShiftActive: enabled => set({ isShiftActive: enabled }),
isCtrlActive: false,
setIsCtrlActive: enabled => set({ isCtrlActive: enabled }),
isAltActive: false,
setIsAltActive: enabled => set({ isAltActive: enabled }),
isMetaActive: false,
setIsMetaActive: enabled => set({ isMetaActive: enabled }),
isAltGrActive: false,
setIsAltGrActive: enabled => set({ isAltGrActive: enabled }),
isPasteModeEnabled: false, isPasteModeEnabled: false,
setPasteModeEnabled: enabled => set({ isPasteModeEnabled: enabled }), setPasteModeEnabled: enabled => set({ isPasteModeEnabled: enabled }),

View File

@ -222,6 +222,10 @@ video::-webkit-media-controls {
background: none; background: none;
} }
.simple-keyboard .hg-button.depressed-key {
@apply border-gray-800! border-b-gray-600! border-t-gray-900! bg-gray-700!;
}
.simple-keyboard .hg-button.selectedButton { .simple-keyboard .hg-button.selectedButton {
background: rgba(5, 25, 70, 0.53); background: rgba(5, 25, 70, 0.53);
@apply text-white; @apply text-white;

View File

@ -5,14 +5,14 @@ export const keys = {
ArrowLeft: 0x50, ArrowLeft: 0x50,
ArrowRight: 0x4f, ArrowRight: 0x4f,
ArrowUp: 0x52, ArrowUp: 0x52,
Backquote: 0x35, Backquote: 0x35, // aka Grave
Backslash: 0x31, Backslash: 0x31,
Backspace: 0x2a, Backspace: 0x2a,
BracketLeft: 0x2f, BracketLeft: 0x2f, // aka LeftBrace
BracketRight: 0x30, BracketRight: 0x30, // aka RightBrace
CapsLock: 0x39, CapsLock: 0x39,
Comma: 0x36, Comma: 0x36,
ContextMenu: 0, Compose: 0x65,
Delete: 0x4c, Delete: 0x4c,
Digit0: 0x27, Digit0: 0x27,
Digit1: 0x1e, Digit1: 0x1e,
@ -41,6 +41,18 @@ export const keys = {
F11: 0x44, F11: 0x44,
F12: 0x45, F12: 0x45,
F13: 0x68, F13: 0x68,
F14: 0x69,
F15: 0x6a,
F16: 0x6b,
F17: 0x6c,
F18: 0x6d,
F19: 0x6e,
F20: 0x6f,
F21: 0x70,
F22: 0x71,
F23: 0x72,
F24: 0x73,
HashTilde: 0x32,
Home: 0x4a, Home: 0x4a,
Insert: 0x49, Insert: 0x49,
IntlBackslash: 0x64, IntlBackslash: 0x64,
@ -70,37 +82,58 @@ export const keys = {
KeyX: 0x1b, KeyX: 0x1b,
KeyY: 0x1c, KeyY: 0x1c,
KeyZ: 0x1d, KeyZ: 0x1d,
KeypadComma: 0x85,
KeypadEqual: 0x86,
KeyRO: 0x87,
KatakanaHiragana: 0x88,
Yen: 0x89,
Henkan: 0x8a,
Muhenkan: 0x8b,
KPJPComma: 0x8c,
International7: 0x8d,
International8: 0x8e,
International9: 0x8f,
Hangeul: 0x90,
Hanja: 0x91,
Katakana: 0x92,
Hiragana: 0x93,
Zenkakuhankaku:0x94,
KeypadLeftParen: 0xb6,
KeypadRightParen: 0xb7,
KeypadExclamation: 0xcf, KeypadExclamation: 0xcf,
Minus: 0x2d, Minus: 0x2d,
NumLock: 0x53, None: 0x00,
Numpad0: 0x62, NumLock: 0x53, // and Clear
Numpad1: 0x59, Numpad0: 0x62, // and Insert
Numpad2: 0x5a, Numpad1: 0x59, // and End
Numpad3: 0x5b, Numpad2: 0x5a, // and Down Arrow
Numpad4: 0x5c, Numpad3: 0x5b, // and Page Down
Numpad4: 0x5c, // and Left Arrow
Numpad5: 0x5d, Numpad5: 0x5d,
Numpad6: 0x5e, Numpad6: 0x5e, // and Right Arrow
Numpad7: 0x5f, Numpad7: 0x5f, // and Home
Numpad8: 0x60, Numpad8: 0x60, // and Up Arrow
Numpad9: 0x61, Numpad9: 0x61, // and Page Up
NumpadAdd: 0x57, NumpadPlus: 0x57,
NumpadDivide: 0x54, NumpadSlash: 0x54,
NumpadEnter: 0x58, NumpadEnter: 0x58,
NumpadEqual: 0x67, NumpadEqual: 0x67,
NumpadMultiply: 0x55, NumpadAsterisk: 0x55,
NumpadSubtract: 0x56, NumpadMinus: 0x56,
NumpadDecimal: 0x63, NumpadDecimal: 0x63, // aka NumpadDot and Delete
Overflow: 0x01,
PageDown: 0x4e, PageDown: 0x4e,
PageUp: 0x4b, PageUp: 0x4b,
Period: 0x37, Period: 0x37, // aka Dot
PrintScreen: 0x46, PrintScreen: 0x46,
Pause: 0x48, Pause: 0x48,
Quote: 0x34, Power: 0x66,
Quote: 0x34, // aka Apostrophe
ScrollLock: 0x47, ScrollLock: 0x47,
Semicolon: 0x33, Semicolon: 0x33,
Slash: 0x38, Slash: 0x38,
Space: 0x2c, Space: 0x2c,
SystemRequest: 0x9a, SystemRequest: 0x9a, // aka Attention
Tab: 0x2b, Tab: 0x2b,
} as Record<string, number>; } as Record<string, number>;
@ -131,23 +164,23 @@ export const keyDisplayMap: Record<string, string> = {
AltMetaEscape: "Alt + Meta + Escape", AltMetaEscape: "Alt + Meta + Escape",
CtrlAltBackspace: "Ctrl + Alt + Backspace", CtrlAltBackspace: "Ctrl + Alt + Backspace",
Escape: "esc", Escape: "esc",
Tab: "tab", Tab: "tab ⇥",
Backspace: "backspace", Backspace: "backspace ⌫",
"(Backspace)": "backspace",
Enter: "enter", Enter: "enter",
CapsLock: "caps lock", CapsLock: "caps lock ⇪",
ShiftLeft: "shift", ShiftLeft: "shift ⇧",
ShiftRight: "shift", ShiftRight: "⇧ shift",
ControlLeft: "ctrl", ControlLeft: "ctrl ⌃",
AltLeft: "alt", ControlRight: "⌃ ctrl",
AltRight: "alt", AltLeft: "alt ⌥",
MetaLeft: "meta", AltRight: "⌥ alt",
MetaRight: "meta", MetaLeft: "meta ⌘", // "meta ⊞" for windows
MetaRight: "⌘ meta",// "≣ meta" for windows
Space: " ", Space: " ",
Insert: "insert", Insert: "ins",
Home: "home", Home: "home",
PageUp: "page up", PageUp: "page up",
Delete: "delete", Delete: "del",
End: "end", End: "end",
PageDown: "page down", PageDown: "page down",
ArrowLeft: "←", ArrowLeft: "←",
@ -213,13 +246,12 @@ export const keyDisplayMap: Record<string, string> = {
Numpad0: "Num 0", Numpad1: "Num 1", Numpad2: "Num 2", Numpad0: "Num 0", Numpad1: "Num 1", Numpad2: "Num 2",
Numpad3: "Num 3", Numpad4: "Num 4", Numpad5: "Num 5", Numpad3: "Num 3", Numpad4: "Num 4", Numpad5: "Num 5",
Numpad6: "Num 6", Numpad7: "Num 7", Numpad8: "Num 8", Numpad6: "Num 6", Numpad7: "Num 7", Numpad8: "Num 8",
Numpad9: "Num 9", NumpadAdd: "Num +", NumpadSubtract: "Num -", Numpad9: "Num 9", NumpadPlus: "Num +", NumpadMinus: "Num -",
NumpadMultiply: "Num *", NumpadDivide: "Num /", NumpadDecimal: "Num .", NumpadAsterisk: "Num *", NumpadSlash: "Num /", NumpadDecimal: "Num .",
NumpadEqual: "Num =", NumpadEnter: "Num Enter", NumpadEqual: "Num =", NumpadEnter: "Num Enter",
NumLock: "Num Lock", NumLock: "Num Lock",
// Modals // Modals
PrintScreen: "prt sc", ScrollLock: "scr lk", Pause: "pause", PrintScreen: "prt sc", ScrollLock: "scr lk", Pause: "pause",
"(PrintScreen)": "sys rq", "(Pause)": "break", "(PrintScreen)": "sys rq", "(Pause)": "break"
SystemRequest: "sys rq", Break: "break"
}; };

View File

@ -42,6 +42,7 @@ import SettingsHardwareRoute from "./routes/devices.$id.settings.hardware";
import SettingsVideoRoute from "./routes/devices.$id.settings.video"; import SettingsVideoRoute from "./routes/devices.$id.settings.video";
import SettingsAppearanceRoute from "./routes/devices.$id.settings.appearance"; import SettingsAppearanceRoute from "./routes/devices.$id.settings.appearance";
import * as SettingsGeneralIndexRoute from "./routes/devices.$id.settings.general._index"; import * as SettingsGeneralIndexRoute from "./routes/devices.$id.settings.general._index";
import SettingsGeneralRebootRoute from "./routes/devices.$id.settings.general.reboot";
import SettingsGeneralUpdateRoute from "./routes/devices.$id.settings.general.update"; import SettingsGeneralUpdateRoute from "./routes/devices.$id.settings.general.update";
import SettingsNetworkRoute from "./routes/devices.$id.settings.network"; import SettingsNetworkRoute from "./routes/devices.$id.settings.network";
import SecurityAccessLocalAuthRoute from "./routes/devices.$id.settings.access.local-auth"; import SecurityAccessLocalAuthRoute from "./routes/devices.$id.settings.access.local-auth";
@ -140,6 +141,10 @@ if (isOnDevice) {
index: true, index: true,
element: <SettingsGeneralIndexRoute.default />, element: <SettingsGeneralIndexRoute.default />,
}, },
{
path: "reboot",
element: <SettingsGeneralRebootRoute />,
},
{ {
path: "update", path: "update",
element: <SettingsGeneralUpdateRoute />, element: <SettingsGeneralUpdateRoute />,

View File

@ -1,28 +0,0 @@
import { Checkbox } from "@/components/Checkbox";
import { SettingsPageHeader } from "@/components/SettingsPageheader";
import { useSettingsStore } from "@/hooks/stores";
import { SettingsItem } from "./devices.$id.settings";
export default function SettingsCtrlAltDelRoute() {
const enableCtrlAltDel = useSettingsStore(state => state.actionBarCtrlAltDel);
const setEnableCtrlAltDel = useSettingsStore(state => state.setActionBarCtrlAltDel);
return (
<div className="space-y-4">
<SettingsPageHeader
title="Action Bar"
description="Customize the action bar of your JetKVM interface"
/>
<div className="space-y-4">
<SettingsItem title="Enable Ctrl-Alt-Del" description="Enable the Ctrl-Alt-Del key on the virtual keyboard">
<Checkbox
checked={enableCtrlAltDel}
onChange={e => setEnableCtrlAltDel(e.target.checked)}
/>
</SettingsItem>
</div>
</div>
);
}

View File

@ -92,6 +92,21 @@ export default function SettingsGeneralRoute() {
/> />
</SettingsItem> </SettingsItem>
</div> </div>
<div className="mt-2 flex items-center justify-between gap-x-2">
<SettingsItem
title="Reboot Device"
description="Power cycle the JetKVM"
/>
<div>
<Button
size="SM"
theme="light"
text="Reboot Device"
onClick={() => navigateTo("./reboot")}
/>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -0,0 +1,66 @@
import { useNavigate } from "react-router-dom";
import { useCallback } from "react";
import { useJsonRpc } from "@/hooks/useJsonRpc";
import { Button } from "@components/Button";
export default function SettingsGeneralRebootRoute() {
const navigate = useNavigate();
const [send] = useJsonRpc();
const onConfirmUpdate = useCallback(() => {
// This is where we send the RPC to the golang binary
send("reboot", {force: true});
}, [send]);
{
/* TODO: Migrate to using URLs instead of the global state. To simplify the refactoring, we'll keep the global state for now. */
}
return <Dialog onClose={() => navigate("..")} onConfirmUpdate={onConfirmUpdate} />;
}
export function Dialog({
onClose,
onConfirmUpdate,
}: {
onClose: () => void;
onConfirmUpdate: () => void;
}) {
return (
<div className="pointer-events-auto relative mx-auto text-left">
<div>
<ConfirmationBox
onYes={onConfirmUpdate}
onNo={onClose}
/>
</div>
</div>
);
}
function ConfirmationBox({
onYes,
onNo,
}: {
onYes: () => void;
onNo: () => void;
}) {
return (
<div className="flex flex-col items-start justify-start space-y-4 text-left">
<div className="text-left">
<p className="text-base font-semibold text-black dark:text-white">
Reboot JetKVM
</p>
<p className="text-sm text-slate-600 dark:text-slate-300">
Do you want to proceed with rebooting the system?
</p>
<div className="mt-4 flex gap-x-2">
<Button size="SM" theme="light" text="Yes" onClick={onYes} />
<Button size="SM" theme="blank" text="No" onClick={onNo} />
</div>
</div>
</div>
);
}

View File

@ -116,15 +116,6 @@ export default function SettingsHardwareRoute() {
}} }}
/> />
</SettingsItem> </SettingsItem>
{/* <SettingsItem
title="Enable Ctrl+Alt+Del Action Bar"
description="Enable or disable the action bar action for sending a Ctrl+Alt+Del to the host"
>
<Checkbox
checked={actionBarConfig.ctrlAltDel}
onChange={onActionBarItemChange("ctrlAltDel")}
/>
</SettingsItem> */}
{settings.backlightSettings.max_brightness != 0 && ( {settings.backlightSettings.max_brightness != 0 && (
<> <>
<SettingsItem <SettingsItem

18
web.go
View File

@ -97,9 +97,6 @@ func setupRouter() *gin.Engine {
// We use this to determine if the device is setup // We use this to determine if the device is setup
r.GET("/device/status", handleDeviceStatus) r.GET("/device/status", handleDeviceStatus)
// We use this to provide the UI with the device configuration
r.GET("/device/ui-config.js", handleDeviceUIConfig)
// We use this to setup the device in the welcome page // We use this to setup the device in the welcome page
r.POST("/device/setup", handleSetup) r.POST("/device/setup", handleSetup)
@ -694,21 +691,6 @@ func handleCloudState(c *gin.Context) {
c.JSON(http.StatusOK, response) c.JSON(http.StatusOK, response)
} }
func handleDeviceUIConfig(c *gin.Context) {
config, _ := json.Marshal(gin.H{
"CLOUD_API": config.CloudURL,
"DEVICE_VERSION": builtAppVersion,
})
if config == nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to marshal config"})
return
}
response := fmt.Sprintf("window.JETKVM_CONFIG = %s;", config)
c.Data(http.StatusOK, "text/javascript; charset=utf-8", []byte(response))
}
func handleSetup(c *gin.Context) { func handleSetup(c *gin.Context) {
// Check if the device is already set up // Check if the device is already set up
if config.LocalAuthMode != "" || config.HashedPassword != "" { if config.LocalAuthMode != "" || config.HashedPassword != "" {