Skip to content

Commit 3a09fcc

Browse files
committed
feat: send all paste keystrokes to backend
1 parent c8dd84c commit 3a09fcc

File tree

7 files changed

+224
-89
lines changed

7 files changed

+224
-89
lines changed

cloud.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,10 @@ func handleSessionRequest(
475475

476476
cloudLogger.Info().Interface("session", session).Msg("new session accepted")
477477
cloudLogger.Trace().Interface("session", session).Msg("new session accepted")
478+
479+
// Cancel any ongoing keyboard report multi when session changes
480+
cancelKeyboardReportMulti()
481+
478482
currentSession = session
479483
_ = wsjson.Write(context.Background(), c, gin.H{"type": "answer", "data": sd})
480484
return nil

hidrpc.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package kvm
22

33
import (
4+
"errors"
45
"fmt"
6+
"io"
57
"time"
68

79
"github.com/jetkvm/kvm/internal/hidrpc"
@@ -143,6 +145,10 @@ func reportHidRPC(params any, session *Session) {
143145
}
144146

145147
if err := session.HidChannel.Send(message); err != nil {
148+
if errors.Is(err, io.ErrClosedPipe) {
149+
logger.Debug().Err(err).Msg("HID RPC channel closed, skipping reportHidRPC")
150+
return
151+
}
146152
logger.Warn().Err(err).Msg("failed to send HID RPC message")
147153
}
148154
}

jsonrpc.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"path/filepath"
1111
"reflect"
1212
"strconv"
13+
"sync"
1314
"time"
1415

1516
"github.com/pion/webrtc/v4"
@@ -1049,6 +1050,101 @@ func rpcSetLocalLoopbackOnly(enabled bool) error {
10491050
return nil
10501051
}
10511052

1053+
// cancelKeyboardReportMulti cancels any ongoing keyboard report multi execution
1054+
func cancelKeyboardReportMulti() {
1055+
keyboardReportMultiLock.Lock()
1056+
defer keyboardReportMultiLock.Unlock()
1057+
1058+
if keyboardReportMultiCancel != nil {
1059+
keyboardReportMultiCancel()
1060+
logger.Info().Msg("canceled keyboard report multi")
1061+
keyboardReportMultiCancel = nil
1062+
}
1063+
}
1064+
1065+
func rpcKeyboardReportMultiWrapper(macro []map[string]any) (usbgadget.KeysDownState, error) {
1066+
keyboardReportMultiLock.Lock()
1067+
defer keyboardReportMultiLock.Unlock()
1068+
1069+
if keyboardReportMultiCancel != nil {
1070+
keyboardReportMultiCancel()
1071+
logger.Info().Msg("canceled previous keyboard report multi")
1072+
}
1073+
1074+
ctx, cancel := context.WithCancel(context.Background())
1075+
keyboardReportMultiCancel = cancel
1076+
1077+
result, err := rpcKeyboardReportMulti(ctx, macro)
1078+
1079+
keyboardReportMultiCancel = nil
1080+
1081+
return result, err
1082+
}
1083+
1084+
var (
1085+
keyboardReportMultiCancel context.CancelFunc
1086+
keyboardReportMultiLock sync.Mutex
1087+
)
1088+
1089+
func rpcKeyboardReportMulti(ctx context.Context, macro []map[string]any) (usbgadget.KeysDownState, error) {
1090+
var last usbgadget.KeysDownState
1091+
var err error
1092+
1093+
logger.Debug().Interface("macro", macro).Msg("Executing keyboard report multi")
1094+
1095+
for i, step := range macro {
1096+
// Check for cancellation before each step
1097+
select {
1098+
case <-ctx.Done():
1099+
logger.Debug().Msg("Keyboard report multi context cancelled")
1100+
return last, ctx.Err()
1101+
default:
1102+
}
1103+
1104+
var modifier byte
1105+
if m, ok := step["modifier"].(float64); ok {
1106+
modifier = byte(int(m))
1107+
} else if mi, ok := step["modifier"].(int); ok {
1108+
modifier = byte(mi)
1109+
} else if mb, ok := step["modifier"].(uint8); ok {
1110+
modifier = mb
1111+
}
1112+
1113+
var keys []byte
1114+
if arr, ok := step["keys"].([]any); ok {
1115+
keys = make([]byte, 0, len(arr))
1116+
for _, v := range arr {
1117+
if f, ok := v.(float64); ok {
1118+
keys = append(keys, byte(int(f)))
1119+
} else if i, ok := v.(int); ok {
1120+
keys = append(keys, byte(i))
1121+
} else if b, ok := v.(uint8); ok {
1122+
keys = append(keys, b)
1123+
}
1124+
}
1125+
} else if bs, ok := step["keys"].([]byte); ok {
1126+
keys = bs
1127+
}
1128+
1129+
// Use context-aware sleep that can be cancelled
1130+
select {
1131+
case <-time.After(100 * time.Millisecond):
1132+
// Sleep completed normally
1133+
case <-ctx.Done():
1134+
logger.Debug().Int("step", i).Msg("Keyboard report multi cancelled during sleep")
1135+
return last, ctx.Err()
1136+
}
1137+
1138+
last, err = rpcKeyboardReport(modifier, keys)
1139+
if err != nil {
1140+
logger.Warn().Err(err).Msg("failed to execute keyboard report multi")
1141+
return last, err
1142+
}
1143+
}
1144+
1145+
return last, nil
1146+
}
1147+
10521148
var rpcHandlers = map[string]RPCHandler{
10531149
"ping": {Func: rpcPing},
10541150
"reboot": {Func: rpcReboot, Params: []string{"force"}},
@@ -1060,6 +1156,7 @@ var rpcHandlers = map[string]RPCHandler{
10601156
"setNetworkSettings": {Func: rpcSetNetworkSettings, Params: []string{"settings"}},
10611157
"renewDHCPLease": {Func: rpcRenewDHCPLease},
10621158
"keyboardReport": {Func: rpcKeyboardReport, Params: []string{"modifier", "keys"}},
1159+
"keyboardReportMulti": {Func: rpcKeyboardReportMultiWrapper, Params: []string{"macro"}},
10631160
"getKeyboardLedState": {Func: rpcGetKeyboardLedState},
10641161
"keypressReport": {Func: rpcKeypressReport, Params: []string{"key", "press"}},
10651162
"getKeyDownState": {Func: rpcGetKeysDownState},

ui/src/components/popovers/PasteModal.tsx

Lines changed: 45 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -8,35 +8,24 @@ import { GridCard } from "@components/Card";
88
import { TextAreaWithLabel } from "@components/TextArea";
99
import { SettingsPageHeader } from "@components/SettingsPageheader";
1010
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
11-
import { useHidStore, useRTCStore, useUiStore, useSettingsStore } from "@/hooks/stores";
12-
import { keys, modifiers } from "@/keyboardMappings";
13-
import { KeyStroke } from "@/keyboardLayouts";
11+
import { useHidStore, useSettingsStore, useUiStore } from "@/hooks/stores";
12+
import useKeyboard from "@/hooks/useKeyboard";
1413
import useKeyboardLayout from "@/hooks/useKeyboardLayout";
1514
import notifications from "@/notifications";
1615

17-
const hidKeyboardPayload = (modifier: number, keys: number[]) => {
18-
return { modifier, keys };
19-
};
20-
21-
const modifierCode = (shift?: boolean, altRight?: boolean) => {
22-
return (shift ? modifiers.ShiftLeft : 0)
23-
| (altRight ? modifiers.AltRight : 0)
24-
}
25-
const noModifier = 0
26-
2716
export default function PasteModal() {
2817
const TextAreaRef = useRef<HTMLTextAreaElement>(null);
2918
const { setPasteModeEnabled } = useHidStore();
3019
const { setDisableVideoFocusTrap } = useUiStore();
3120

3221
const { send } = useJsonRpc();
33-
const { rpcDataChannel } = useRTCStore();
22+
const { executeMacro } = useKeyboard();
3423

3524
const [invalidChars, setInvalidChars] = useState<string[]>([]);
3625
const close = useClose();
3726

3827
const { setKeyboardLayout } = useSettingsStore();
39-
const { selectedKeyboard } = useKeyboardLayout();
28+
const { selectedKeyboard } = useKeyboardLayout();
4029

4130
useEffect(() => {
4231
send("getKeyboardLayout", {}, (resp: JsonRpcResponse) => {
@@ -52,15 +41,20 @@ export default function PasteModal() {
5241
}, [setDisableVideoFocusTrap, setPasteModeEnabled]);
5342

5443
const onConfirmPaste = useCallback(async () => {
55-
setPasteModeEnabled(false);
56-
setDisableVideoFocusTrap(false);
44+
// setPasteModeEnabled(false);
45+
// setDisableVideoFocusTrap(false);
5746

58-
if (rpcDataChannel?.readyState !== "open" || !TextAreaRef.current) return;
59-
if (!selectedKeyboard) return;
47+
if (!TextAreaRef.current || !selectedKeyboard) return;
6048

6149
const text = TextAreaRef.current.value;
6250

6351
try {
52+
const macroSteps: {
53+
keys: string[] | null;
54+
modifiers: string[] | null;
55+
delay: number;
56+
}[] = [];
57+
6458
for (const char of text) {
6559
const keyprops = selectedKeyboard.chars[char];
6660
if (!keyprops) continue;
@@ -70,39 +64,41 @@ export default function PasteModal() {
7064

7165
// if this is an accented character, we need to send that accent FIRST
7266
if (accentKey) {
73-
await sendKeystroke({modifier: modifierCode(accentKey.shift, accentKey.altRight), keys: [ keys[accentKey.key] ] })
67+
const accentModifiers: string[] = [];
68+
if (accentKey.shift) accentModifiers.push("ShiftLeft");
69+
if (accentKey.altRight) accentModifiers.push("AltRight");
70+
71+
macroSteps.push({
72+
keys: [String(accentKey.key)],
73+
modifiers: accentModifiers.length > 0 ? accentModifiers : null,
74+
delay: 100,
75+
});
7476
}
7577

7678
// now send the actual key
77-
await sendKeystroke({ modifier: modifierCode(shift, altRight), keys: [ keys[key] ]});
79+
const modifiers: string[] = [];
80+
if (shift) modifiers.push("ShiftLeft");
81+
if (altRight) modifiers.push("AltRight");
82+
83+
macroSteps.push({
84+
keys: [String(key)],
85+
modifiers: modifiers.length > 0 ? modifiers : null,
86+
delay: 100,
87+
});
7888

7989
// if what was requested was a dead key, we need to send an unmodified space to emit
8090
// just the accent character
81-
if (deadKey) {
82-
await sendKeystroke({ modifier: noModifier, keys: [ keys["Space"] ] });
83-
}
91+
if (deadKey) macroSteps.push({ keys: ["Space"], modifiers: null, delay: 100 });
92+
}
8493

85-
// now send a message with no keys down to "release" the keys
86-
await sendKeystroke({ modifier: 0, keys: [] });
94+
if (macroSteps.length > 0) {
95+
await executeMacro(macroSteps);
8796
}
8897
} catch (error) {
8998
console.error("Failed to paste text:", error);
9099
notifications.error("Failed to paste text");
91100
}
92-
93-
async function sendKeystroke(stroke: KeyStroke) {
94-
await new Promise<void>((resolve, reject) => {
95-
send(
96-
"keyboardReport",
97-
hidKeyboardPayload(stroke.modifier, stroke.keys),
98-
params => {
99-
if ("error" in params) return reject(params.error);
100-
resolve();
101-
}
102-
);
103-
});
104-
}
105-
}, [selectedKeyboard, rpcDataChannel?.readyState, send, setDisableVideoFocusTrap, setPasteModeEnabled]);
101+
}, [selectedKeyboard, executeMacro]);
106102

107103
useEffect(() => {
108104
if (TextAreaRef.current) {
@@ -122,14 +118,18 @@ export default function PasteModal() {
122118
/>
123119

124120
<div
125-
className="animate-fadeIn opacity-0 space-y-2"
121+
className="animate-fadeIn space-y-2 opacity-0"
126122
style={{
127123
animationDuration: "0.7s",
128124
animationDelay: "0.1s",
129125
}}
130126
>
131127
<div>
132-
<div className="w-full" onKeyUp={e => e.stopPropagation()} onKeyDown={e => e.stopPropagation()}>
128+
<div
129+
className="w-full"
130+
onKeyUp={e => e.stopPropagation()}
131+
onKeyDown={e => e.stopPropagation()}
132+
>
133133
<TextAreaWithLabel
134134
ref={TextAreaRef}
135135
label="Paste from host"
@@ -173,15 +173,16 @@ export default function PasteModal() {
173173
</div>
174174
<div className="space-y-4">
175175
<p className="text-xs text-slate-600 dark:text-slate-400">
176-
Sending text using keyboard layout: {selectedKeyboard.isoCode}-{selectedKeyboard.name}
176+
Sending text using keyboard layout: {selectedKeyboard.isoCode}-
177+
{selectedKeyboard.name}
177178
</p>
178179
</div>
179180
</div>
180181
</div>
181182
</div>
182183
</div>
183184
<div
184-
className="flex animate-fadeIn opacity-0 items-center justify-end gap-x-2"
185+
className="flex animate-fadeIn items-center justify-end gap-x-2 opacity-0"
185186
style={{
186187
animationDuration: "0.7s",
187188
animationDelay: "0.2s",

0 commit comments

Comments
 (0)