Skip to content

Commit 9a56560

Browse files
authored
fix: arrow keys on windows (#661)
1 parent da0863b commit 9a56560

File tree

2 files changed

+98
-34
lines changed

2 files changed

+98
-34
lines changed

packages/cli/src/ui/contexts/KeypressContext.test.tsx

Lines changed: 93 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -526,7 +526,7 @@ describe('KeypressContext - Kitty Protocol', () => {
526526
});
527527

528528
await waitFor(() => {
529-
expect(keyHandler).toHaveBeenCalledTimes(2); // 1 paste event + 1 paste event for 'after'
529+
expect(keyHandler).toHaveBeenCalledTimes(6); // 1 paste event + 5 individual chars for 'after'
530530
});
531531

532532
// Should emit paste event first
@@ -538,12 +538,40 @@ describe('KeypressContext - Kitty Protocol', () => {
538538
}),
539539
);
540540

541-
// Then process 'after' as a paste event (since it's > 2 chars)
541+
// Then process 'after' as individual characters (since it doesn't contain return)
542542
expect(keyHandler).toHaveBeenNthCalledWith(
543543
2,
544544
expect.objectContaining({
545-
paste: true,
546-
sequence: 'after',
545+
name: 'a',
546+
paste: false,
547+
}),
548+
);
549+
expect(keyHandler).toHaveBeenNthCalledWith(
550+
3,
551+
expect.objectContaining({
552+
name: 'f',
553+
paste: false,
554+
}),
555+
);
556+
expect(keyHandler).toHaveBeenNthCalledWith(
557+
4,
558+
expect.objectContaining({
559+
name: 't',
560+
paste: false,
561+
}),
562+
);
563+
expect(keyHandler).toHaveBeenNthCalledWith(
564+
5,
565+
expect.objectContaining({
566+
name: 'e',
567+
paste: false,
568+
}),
569+
);
570+
expect(keyHandler).toHaveBeenNthCalledWith(
571+
6,
572+
expect.objectContaining({
573+
name: 'r',
574+
paste: false,
547575
}),
548576
);
549577
});
@@ -571,7 +599,7 @@ describe('KeypressContext - Kitty Protocol', () => {
571599
});
572600

573601
await waitFor(() => {
574-
expect(keyHandler).toHaveBeenCalledTimes(14); // Adjusted based on actual behavior
602+
expect(keyHandler).toHaveBeenCalledTimes(16); // 5 + 1 + 6 + 1 + 3 = 16 calls
575603
});
576604

577605
// Check the sequence: 'start' (5 chars) + paste1 + 'middle' (6 chars) + paste2 + 'end' (3 chars as paste)
@@ -643,13 +671,18 @@ describe('KeypressContext - Kitty Protocol', () => {
643671
}),
644672
);
645673

646-
// 'end' as paste event (since it's > 2 chars)
674+
// 'end' as individual characters (since it doesn't contain return)
647675
expect(keyHandler).toHaveBeenNthCalledWith(
648676
callIndex++,
649-
expect.objectContaining({
650-
paste: true,
651-
sequence: 'end',
652-
}),
677+
expect.objectContaining({ name: 'e' }),
678+
);
679+
expect(keyHandler).toHaveBeenNthCalledWith(
680+
callIndex++,
681+
expect.objectContaining({ name: 'n' }),
682+
);
683+
expect(keyHandler).toHaveBeenNthCalledWith(
684+
callIndex++,
685+
expect.objectContaining({ name: 'd' }),
653686
);
654687
});
655688

@@ -738,16 +771,18 @@ describe('KeypressContext - Kitty Protocol', () => {
738771
});
739772

740773
await waitFor(() => {
741-
// With the current implementation, fragmented data gets processed differently
742-
// The first fragment '\x1b[20' gets processed as individual characters
743-
// The second fragment '0~content\x1b[2' gets processed as paste + individual chars
744-
// The third fragment '01~' gets processed as individual characters
745-
expect(keyHandler).toHaveBeenCalled();
774+
// With the current implementation, fragmented paste markers get reconstructed
775+
// into a single paste event for 'content'
776+
expect(keyHandler).toHaveBeenCalledTimes(1);
746777
});
747778

748-
// The current implementation processes fragmented paste markers as separate events
749-
// rather than reconstructing them into a single paste event
750-
expect(keyHandler.mock.calls.length).toBeGreaterThan(1);
779+
// Should reconstruct the fragmented paste markers into a single paste event
780+
expect(keyHandler).toHaveBeenCalledWith(
781+
expect.objectContaining({
782+
paste: true,
783+
sequence: 'content',
784+
}),
785+
);
751786
});
752787
});
753788

@@ -851,36 +886,55 @@ describe('KeypressContext - Kitty Protocol', () => {
851886
stdin.emit('data', Buffer.from('lo'));
852887
});
853888

854-
// With the current implementation, data is processed as it arrives
855-
// First chunk 'hel' is treated as paste (multi-character)
889+
// With the current implementation, data is processed as individual characters
890+
// since 'hel' doesn't contain return (0x0d)
856891
expect(keyHandler).toHaveBeenNthCalledWith(
857892
1,
858893
expect.objectContaining({
859-
paste: true,
860-
sequence: 'hel',
894+
name: 'h',
895+
sequence: 'h',
896+
paste: false,
861897
}),
862898
);
863899

864-
// Second chunk 'lo' is processed as individual characters
865900
expect(keyHandler).toHaveBeenNthCalledWith(
866901
2,
902+
expect.objectContaining({
903+
name: 'e',
904+
sequence: 'e',
905+
paste: false,
906+
}),
907+
);
908+
909+
expect(keyHandler).toHaveBeenNthCalledWith(
910+
3,
867911
expect.objectContaining({
868912
name: 'l',
869913
sequence: 'l',
870914
paste: false,
871915
}),
872916
);
873917

918+
// Second chunk 'lo' is also processed as individual characters
874919
expect(keyHandler).toHaveBeenNthCalledWith(
875-
3,
920+
4,
921+
expect.objectContaining({
922+
name: 'l',
923+
sequence: 'l',
924+
paste: false,
925+
}),
926+
);
927+
928+
expect(keyHandler).toHaveBeenNthCalledWith(
929+
5,
876930
expect.objectContaining({
877931
name: 'o',
878932
sequence: 'o',
879933
paste: false,
880934
}),
881935
);
882936

883-
expect(keyHandler).toHaveBeenCalledTimes(3);
937+
expect(keyHandler).toHaveBeenCalledTimes(5);
884938
} finally {
885939
vi.useRealTimers();
886940
}
@@ -907,14 +961,20 @@ describe('KeypressContext - Kitty Protocol', () => {
907961
});
908962

909963
// Should flush immediately without waiting for timeout
910-
// Large data gets treated as paste event
911-
expect(keyHandler).toHaveBeenCalledTimes(1);
912-
expect(keyHandler).toHaveBeenCalledWith(
913-
expect.objectContaining({
914-
paste: true,
915-
sequence: largeData,
916-
}),
917-
);
964+
// Large data without return gets treated as individual characters
965+
expect(keyHandler).toHaveBeenCalledTimes(65);
966+
967+
// Each character should be processed individually
968+
for (let i = 0; i < 65; i++) {
969+
expect(keyHandler).toHaveBeenNthCalledWith(
970+
i + 1,
971+
expect.objectContaining({
972+
name: 'x',
973+
sequence: 'x',
974+
paste: false,
975+
}),
976+
);
977+
}
918978

919979
// Advancing timer should not cause additional calls
920980
const callCountBefore = keyHandler.mock.calls.length;

packages/cli/src/ui/contexts/KeypressContext.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -407,7 +407,11 @@ export function KeypressProvider({
407407
return;
408408
}
409409

410-
if (rawDataBuffer.length <= 2 || isPaste) {
410+
if (
411+
(rawDataBuffer.length <= 2 && rawDataBuffer.includes(0x0d)) ||
412+
!rawDataBuffer.includes(0x0d) ||
413+
isPaste
414+
) {
411415
keypressStream.write(rawDataBuffer);
412416
} else {
413417
// Flush raw data buffer as a paste event

0 commit comments

Comments
 (0)