@@ -2,6 +2,10 @@
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed duplicate printable characters from Kitty keyboard protocol CSI-u plus raw character input on layouts such as Italian ([#3780](https://github.com/badlogic/pi-mono/issues/3780))
|
||||||
|
|
||||||
## [0.70.2] - 2026-04-24
|
## [0.70.2] - 2026-04-24
|
||||||
|
|
||||||
## [0.70.1] - 2026-04-24
|
## [0.70.1] - 2026-04-24
|
||||||
|
|||||||
@@ -181,6 +181,14 @@ function isCompleteApcSequence(data: string): "complete" | "incomplete" {
|
|||||||
/**
|
/**
|
||||||
* Split accumulated buffer into complete sequences
|
* Split accumulated buffer into complete sequences
|
||||||
*/
|
*/
|
||||||
|
function parseUnmodifiedKittyPrintableCodepoint(sequence: string): number | undefined {
|
||||||
|
const match = sequence.match(/^\x1b\[(\d+)(?::\d*)?(?::\d+)?u$/);
|
||||||
|
if (!match) return undefined;
|
||||||
|
|
||||||
|
const codepoint = parseInt(match[1]!, 10);
|
||||||
|
return codepoint >= 32 ? codepoint : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
function extractCompleteSequences(buffer: string): { sequences: string[]; remainder: string } {
|
function extractCompleteSequences(buffer: string): { sequences: string[]; remainder: string } {
|
||||||
const sequences: string[] = [];
|
const sequences: string[] = [];
|
||||||
let pos = 0;
|
let pos = 0;
|
||||||
@@ -246,6 +254,7 @@ export class StdinBuffer extends EventEmitter<StdinBufferEventMap> {
|
|||||||
private readonly timeoutMs: number;
|
private readonly timeoutMs: number;
|
||||||
private pasteMode: boolean = false;
|
private pasteMode: boolean = false;
|
||||||
private pasteBuffer: string = "";
|
private pasteBuffer: string = "";
|
||||||
|
private pendingKittyPrintableCodepoint: number | undefined;
|
||||||
|
|
||||||
constructor(options: StdinBufferOptions = {}) {
|
constructor(options: StdinBufferOptions = {}) {
|
||||||
super();
|
super();
|
||||||
@@ -274,7 +283,7 @@ export class StdinBuffer extends EventEmitter<StdinBufferEventMap> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (str.length === 0 && this.buffer.length === 0) {
|
if (str.length === 0 && this.buffer.length === 0) {
|
||||||
this.emit("data", "");
|
this.emitDataSequence("");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -291,6 +300,7 @@ export class StdinBuffer extends EventEmitter<StdinBufferEventMap> {
|
|||||||
|
|
||||||
this.pasteMode = false;
|
this.pasteMode = false;
|
||||||
this.pasteBuffer = "";
|
this.pasteBuffer = "";
|
||||||
|
this.pendingKittyPrintableCodepoint = undefined;
|
||||||
|
|
||||||
this.emit("paste", pastedContent);
|
this.emit("paste", pastedContent);
|
||||||
|
|
||||||
@@ -307,10 +317,11 @@ export class StdinBuffer extends EventEmitter<StdinBufferEventMap> {
|
|||||||
const beforePaste = this.buffer.slice(0, startIndex);
|
const beforePaste = this.buffer.slice(0, startIndex);
|
||||||
const result = extractCompleteSequences(beforePaste);
|
const result = extractCompleteSequences(beforePaste);
|
||||||
for (const sequence of result.sequences) {
|
for (const sequence of result.sequences) {
|
||||||
this.emit("data", sequence);
|
this.emitDataSequence(sequence);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.pendingKittyPrintableCodepoint = undefined;
|
||||||
this.buffer = this.buffer.slice(startIndex + BRACKETED_PASTE_START.length);
|
this.buffer = this.buffer.slice(startIndex + BRACKETED_PASTE_START.length);
|
||||||
this.pasteMode = true;
|
this.pasteMode = true;
|
||||||
this.pasteBuffer = this.buffer;
|
this.pasteBuffer = this.buffer;
|
||||||
@@ -323,6 +334,7 @@ export class StdinBuffer extends EventEmitter<StdinBufferEventMap> {
|
|||||||
|
|
||||||
this.pasteMode = false;
|
this.pasteMode = false;
|
||||||
this.pasteBuffer = "";
|
this.pasteBuffer = "";
|
||||||
|
this.pendingKittyPrintableCodepoint = undefined;
|
||||||
|
|
||||||
this.emit("paste", pastedContent);
|
this.emit("paste", pastedContent);
|
||||||
|
|
||||||
@@ -337,7 +349,7 @@ export class StdinBuffer extends EventEmitter<StdinBufferEventMap> {
|
|||||||
this.buffer = result.remainder;
|
this.buffer = result.remainder;
|
||||||
|
|
||||||
for (const sequence of result.sequences) {
|
for (const sequence of result.sequences) {
|
||||||
this.emit("data", sequence);
|
this.emitDataSequence(sequence);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.buffer.length > 0) {
|
if (this.buffer.length > 0) {
|
||||||
@@ -345,12 +357,23 @@ export class StdinBuffer extends EventEmitter<StdinBufferEventMap> {
|
|||||||
const flushed = this.flush();
|
const flushed = this.flush();
|
||||||
|
|
||||||
for (const sequence of flushed) {
|
for (const sequence of flushed) {
|
||||||
this.emit("data", sequence);
|
this.emitDataSequence(sequence);
|
||||||
}
|
}
|
||||||
}, this.timeoutMs);
|
}, this.timeoutMs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private emitDataSequence(sequence: string): void {
|
||||||
|
const rawCodepoint = sequence.length === 1 ? sequence.codePointAt(0) : undefined;
|
||||||
|
if (rawCodepoint !== undefined && rawCodepoint === this.pendingKittyPrintableCodepoint) {
|
||||||
|
this.pendingKittyPrintableCodepoint = undefined;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pendingKittyPrintableCodepoint = parseUnmodifiedKittyPrintableCodepoint(sequence);
|
||||||
|
this.emit("data", sequence);
|
||||||
|
}
|
||||||
|
|
||||||
flush(): string[] {
|
flush(): string[] {
|
||||||
if (this.timeout) {
|
if (this.timeout) {
|
||||||
clearTimeout(this.timeout);
|
clearTimeout(this.timeout);
|
||||||
@@ -363,6 +386,7 @@ export class StdinBuffer extends EventEmitter<StdinBufferEventMap> {
|
|||||||
|
|
||||||
const sequences = [this.buffer];
|
const sequences = [this.buffer];
|
||||||
this.buffer = "";
|
this.buffer = "";
|
||||||
|
this.pendingKittyPrintableCodepoint = undefined;
|
||||||
return sequences;
|
return sequences;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -374,6 +398,7 @@ export class StdinBuffer extends EventEmitter<StdinBufferEventMap> {
|
|||||||
this.buffer = "";
|
this.buffer = "";
|
||||||
this.pasteMode = false;
|
this.pasteMode = false;
|
||||||
this.pasteBuffer = "";
|
this.pasteBuffer = "";
|
||||||
|
this.pendingKittyPrintableCodepoint = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
getBuffer(): string {
|
getBuffer(): string {
|
||||||
|
|||||||
@@ -204,9 +204,25 @@ describe("StdinBuffer", () => {
|
|||||||
assert.deepStrictEqual(emittedSequences, ["a", "\x1b[97;1:3u"]);
|
assert.deepStrictEqual(emittedSequences, ["a", "\x1b[97;1:3u"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle Kitty sequence followed by plain characters", () => {
|
it("should drop raw duplicate character after matching Kitty printable sequence", () => {
|
||||||
processInput("\x1b[97ua");
|
processInput("\x1b[224uà");
|
||||||
assert.deepStrictEqual(emittedSequences, ["\x1b[97u", "a"]);
|
assert.deepStrictEqual(emittedSequences, ["\x1b[224u"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should drop raw duplicate character after matching Kitty printable sequence across chunks", () => {
|
||||||
|
processInput("\x1b[64u");
|
||||||
|
processInput("@");
|
||||||
|
assert.deepStrictEqual(emittedSequences, ["\x1b[64u"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should keep non-matching plain character after Kitty printable sequence", () => {
|
||||||
|
processInput("\x1b[97ub");
|
||||||
|
assert.deepStrictEqual(emittedSequences, ["\x1b[97u", "b"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should keep raw character after modified Kitty printable sequence", () => {
|
||||||
|
processInput("\x1b[64;3u@");
|
||||||
|
assert.deepStrictEqual(emittedSequences, ["\x1b[64;3u", "@"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle rapid typing simulation with Kitty protocol", () => {
|
it("should handle rapid typing simulation with Kitty protocol", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user