feat(keys): migrated keyboard parsing logic from JavaScript to native Rust implementation
- Migrated keyboard parsing and matching logic from JavaScript to native Rust implementation for improved performance.
- Exported new native functions parseKey, parseKittySequence, matchesLegacySequence, and matchesKey from @oh-my-pi/pi-natives package.
- Exported KeyEventType enum and ParsedKittyResult type for Kitty keyboard protocol support.
- Removed Key helper object from public API of @oh-my-pi/pi-tui package; users should now pass key identifiers as strings (e.g., 'ctrl+c' instead of Key.ctrl('c')).
- Added comprehensive keyboard parsing support in native layer including Kitty protocol, legacy terminal sequences, function keys, and modifier combinations.
- Added phf dependency for efficient perfect hash map lookups of legacy terminal escape sequences.
This commit is contained in:
Generated
+66
-8
@@ -749,13 +749,23 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078"
|
||||
dependencies = [
|
||||
"phf_macros",
|
||||
"phf_shared 0.11.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf"
|
||||
version = "0.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf"
|
||||
dependencies = [
|
||||
"phf_shared",
|
||||
"phf_shared 0.13.1",
|
||||
"serde",
|
||||
]
|
||||
|
||||
@@ -765,8 +775,18 @@ version = "0.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1"
|
||||
dependencies = [
|
||||
"phf_generator",
|
||||
"phf_shared",
|
||||
"phf_generator 0.13.1",
|
||||
"phf_shared 0.13.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_generator"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
|
||||
dependencies = [
|
||||
"phf_shared 0.11.3",
|
||||
"rand",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -776,7 +796,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"phf_shared",
|
||||
"phf_shared 0.13.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_macros"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216"
|
||||
dependencies = [
|
||||
"phf_generator 0.11.3",
|
||||
"phf_shared 0.11.3",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_shared"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5"
|
||||
dependencies = [
|
||||
"siphasher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -803,6 +845,7 @@ dependencies = [
|
||||
"napi",
|
||||
"napi-build",
|
||||
"napi-derive",
|
||||
"phf 0.11.3",
|
||||
"rayon",
|
||||
"syntect",
|
||||
"unicode-segmentation",
|
||||
@@ -879,6 +922,21 @@ version = "5.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
||||
dependencies = [
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||
|
||||
[[package]]
|
||||
name = "rayon"
|
||||
version = "1.11.0"
|
||||
@@ -1025,7 +1083,7 @@ checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901"
|
||||
dependencies = [
|
||||
"new_debug_unreachable",
|
||||
"parking_lot",
|
||||
"phf_shared",
|
||||
"phf_shared 0.13.1",
|
||||
"precomputed-hash",
|
||||
]
|
||||
|
||||
@@ -1035,8 +1093,8 @@ version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69"
|
||||
dependencies = [
|
||||
"phf_generator",
|
||||
"phf_shared",
|
||||
"phf_generator 0.13.1",
|
||||
"phf_shared 0.13.1",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
]
|
||||
@@ -1171,7 +1229,7 @@ version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57a9779e9f04d2ac1ce317aee707aa2f6b773afba7b931222bff6983843b1576"
|
||||
dependencies = [
|
||||
"phf",
|
||||
"phf 0.13.1",
|
||||
"phf_codegen",
|
||||
"string_cache",
|
||||
"string_cache_codegen",
|
||||
|
||||
@@ -36,6 +36,7 @@ syntect = { version = "5.3", default-features = false, features = [
|
||||
"regex-fancy",
|
||||
] }
|
||||
html-to-markdown-rs = { version = "2.24", default-features = false }
|
||||
phf = { version = "0.11", features = ["macros"] }
|
||||
|
||||
[build-dependencies]
|
||||
napi-build = "2"
|
||||
|
||||
+1007
-86
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,16 @@
|
||||
# Changelog
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- Exported keyboard parsing and matching functions: `parseKey`, `parseKittySequence`, `matchesLegacySequence`, and `matchesKey` for terminal input handling
|
||||
- Exported `KeyEventType` enum and `ParsedKittyResult` type for Kitty keyboard protocol support
|
||||
- Added `parseKey` function to parse terminal input and return normalized key identifiers (e.g., "ctrl+c", "shift+tab")
|
||||
- Added `parseKittySequence` function to parse Kitty keyboard protocol sequences with codepoint, modifier, and event type information
|
||||
- Added `matchesLegacySequence` function to match legacy escape sequences for specific keys
|
||||
- Added `matchesKey` function to match input against key identifiers with support for modifiers and Kitty protocol
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved native binary installation to use atomic rename operations and better fallback handling for Windows DLLs
|
||||
|
||||
@@ -97,7 +97,15 @@ export {
|
||||
// Keyboard sequence helpers
|
||||
// =============================================================================
|
||||
|
||||
export { matchesKittySequence } from "./keys/index";
|
||||
export {
|
||||
type KeyEventType,
|
||||
matchesKey,
|
||||
matchesKittySequence,
|
||||
matchesLegacySequence,
|
||||
type ParsedKittyResult,
|
||||
parseKey,
|
||||
parseKittySequence,
|
||||
} from "./keys/index";
|
||||
|
||||
// =============================================================================
|
||||
// HTML to Markdown
|
||||
@@ -107,5 +115,3 @@ export {
|
||||
type HtmlToMarkdownOptions,
|
||||
htmlToMarkdown,
|
||||
} from "./html/index";
|
||||
|
||||
export type { RequestOptions } from "./request-options";
|
||||
|
||||
@@ -2,9 +2,63 @@
|
||||
* Keyboard sequence utilities powered by native bindings.
|
||||
*/
|
||||
|
||||
import { native } from "../native";
|
||||
import { type KeyEventType, native, type ParsedKittyResult } from "../native";
|
||||
|
||||
export type { KeyEventType, ParsedKittyResult };
|
||||
|
||||
/** Match Kitty protocol sequences for codepoint and modifier. */
|
||||
export function matchesKittySequence(data: string, expectedCodepoint: number, expectedModifier: number): boolean {
|
||||
return native.matchesKittySequence(data, expectedCodepoint, expectedModifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a Kitty keyboard protocol sequence.
|
||||
*
|
||||
* @param data - Raw escape sequence from terminal
|
||||
* @returns Parsed sequence with codepoint, modifier, and event type, or undefined if not a valid Kitty sequence
|
||||
*/
|
||||
export function parseKittySequence(data: string): ParsedKittyResult | undefined {
|
||||
return native.parseKittySequence(data) ?? undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse terminal input and return a normalized key identifier.
|
||||
*
|
||||
* Returns key names like "escape", "ctrl+c", "shift+tab", "alt+enter".
|
||||
* Returns undefined if the input is not a recognized key sequence.
|
||||
*
|
||||
* @param data - Raw input data from terminal
|
||||
* @param kittyProtocolActive - Whether Kitty keyboard protocol is active
|
||||
*/
|
||||
export function parseKey(data: string, kittyProtocolActive: boolean): string | undefined {
|
||||
return native.parseKey(data, kittyProtocolActive) ?? undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if input matches a legacy escape sequence for a specific key.
|
||||
*
|
||||
* @param data - Raw input data from terminal
|
||||
* @param keyName - Key name to match (e.g., "up", "f1", "ctrl+up")
|
||||
*/
|
||||
export function matchesLegacySequence(data: string, keyName: string): boolean {
|
||||
return native.matchesLegacySequence(data, keyName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Match input data against a key identifier string.
|
||||
*
|
||||
* Supported key identifiers:
|
||||
* - Single keys: "escape", "tab", "enter", "backspace", "delete", "home", "end", "space"
|
||||
* - Arrow keys: "up", "down", "left", "right"
|
||||
* - Ctrl combinations: "ctrl+c", "ctrl+z", etc.
|
||||
* - Shift combinations: "shift+tab", "shift+enter"
|
||||
* - Alt combinations: "alt+enter", "alt+backspace"
|
||||
* - Combined modifiers: "shift+ctrl+p", "ctrl+alt+x"
|
||||
*
|
||||
* @param data - Raw input data from terminal
|
||||
* @param keyId - Key identifier (e.g., "ctrl+c", "escape")
|
||||
* @param kittyProtocolActive - Whether Kitty keyboard protocol is active
|
||||
*/
|
||||
export function matchesKey(data: string, keyId: string, kittyProtocolActive: boolean): boolean {
|
||||
return native.matchesKey(data, keyId, kittyProtocolActive);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,26 @@ import type { HighlightColors } from "./highlight/index";
|
||||
import type { HtmlToMarkdownOptions } from "./html/types";
|
||||
import type { ExtractSegmentsResult, SliceWithWidthResult } from "./text/index";
|
||||
|
||||
export type { RequestOptions } from "./request-options";
|
||||
|
||||
/**
|
||||
* Event types from Kitty keyboard protocol (flag 2)
|
||||
* 1 = key press, 2 = key repeat, 3 = key release
|
||||
*/
|
||||
export const enum KeyEventType {
|
||||
Press = 1,
|
||||
Repeat = 2,
|
||||
Release = 3,
|
||||
}
|
||||
/** Parsed Kitty keyboard protocol sequence result. */
|
||||
export interface ParsedKittyResult {
|
||||
codepoint: number;
|
||||
shiftedKey?: number;
|
||||
baseLayoutKey?: number;
|
||||
modifier: number;
|
||||
eventType?: KeyEventType;
|
||||
}
|
||||
|
||||
export interface NativePhotonImage {
|
||||
getWidth(): number;
|
||||
getHeight(): number;
|
||||
@@ -66,6 +86,10 @@ export interface NativeBindings {
|
||||
strictAfter: boolean,
|
||||
): ExtractSegmentsResult;
|
||||
matchesKittySequence(data: string, expectedCodepoint: number, expectedModifier: number): boolean;
|
||||
parseKey(data: string, kittyProtocolActive: boolean): string | null;
|
||||
matchesLegacySequence(data: string, keyName: string): boolean;
|
||||
parseKittySequence(data: string): ParsedKittyResult | null;
|
||||
matchesKey(data: string, keyId: string, kittyProtocolActive: boolean): boolean;
|
||||
}
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
@@ -141,6 +165,11 @@ function validateNative(bindings: NativeBindings, source: string): void {
|
||||
checkFn("sliceWithWidth");
|
||||
checkFn("extractSegments");
|
||||
checkFn("matchesKittySequence");
|
||||
checkFn("parseKey");
|
||||
checkFn("matchesLegacySequence");
|
||||
checkFn("parseKittySequence");
|
||||
checkFn("matchesKey");
|
||||
checkFn("visibleWidth");
|
||||
|
||||
if (missing.length) {
|
||||
throw new Error(
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
# Changelog
|
||||
|
||||
## [Unreleased]
|
||||
### Breaking Changes
|
||||
|
||||
- Removed `Key` helper object from public API; use string literals like `"ctrl+c"` instead of `Key.ctrl("c")`
|
||||
- Removed `KeyEventType` export from public API
|
||||
|
||||
### Changed
|
||||
|
||||
- Migrated key parsing and matching logic to native implementation for improved performance
|
||||
- Simplified `isKeyRelease()` and `isKeyRepeat()` to use regex pattern matching instead of string inclusion checks
|
||||
|
||||
## [9.6.2] - 2026-02-01
|
||||
### Changed
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,116 @@
|
||||
import { parseKey as nativeParseKey } from "@oh-my-pi/pi-natives";
|
||||
import * as native from "../src/keys";
|
||||
import * as js from "./_jskey";
|
||||
|
||||
const ITERATIONS = 2000;
|
||||
|
||||
// Test cases covering various input types
|
||||
const samples = [
|
||||
// Kitty protocol sequences
|
||||
{ name: "kitty ctrl+a", data: "\x1b[97;5u", expected: "ctrl+a" },
|
||||
{ name: "kitty shift+tab", data: "\x1b[9;2u", expected: "shift+tab" },
|
||||
{ name: "kitty alt+enter", data: "\x1b[13;3u", expected: "alt+enter" },
|
||||
{ name: "kitty ctrl+right", data: "\x1b[1;5C", expected: "ctrl+right" },
|
||||
{ name: "kitty shift+delete", data: "\x1b[3;2~", expected: "shift+delete" },
|
||||
{ name: "kitty base-layout", data: "\x1b[108::97;5u", expected: "ctrl+a" },
|
||||
|
||||
// Legacy sequences
|
||||
{ name: "legacy escape", data: "\x1b", expected: "escape" },
|
||||
{ name: "legacy tab", data: "\t", expected: "tab" },
|
||||
{ name: "legacy enter", data: "\r", expected: "enter" },
|
||||
{ name: "legacy space", data: " ", expected: "space" },
|
||||
{ name: "legacy backspace", data: "\x7f", expected: "backspace" },
|
||||
{ name: "legacy shift+tab", data: "\x1b[Z", expected: "shift+tab" },
|
||||
{ name: "legacy up", data: "\x1b[A", expected: "up" },
|
||||
{ name: "legacy down", data: "\x1b[B", expected: "down" },
|
||||
{ name: "legacy left", data: "\x1b[D", expected: "left" },
|
||||
{ name: "legacy right", data: "\x1b[C", expected: "right" },
|
||||
{ name: "legacy home", data: "\x1b[H", expected: "home" },
|
||||
{ name: "legacy end", data: "\x1b[F", expected: "end" },
|
||||
{ name: "legacy delete", data: "\x1b[3~", expected: "delete" },
|
||||
{ name: "legacy pageUp", data: "\x1b[5~", expected: "pageUp" },
|
||||
{ name: "legacy pageDown", data: "\x1b[6~", expected: "pageDown" },
|
||||
|
||||
// Function keys
|
||||
{ name: "legacy f1", data: "\x1bOP", expected: "f1" },
|
||||
{ name: "legacy f5", data: "\x1b[15~", expected: "f5" },
|
||||
{ name: "legacy f12", data: "\x1b[24~", expected: "f12" },
|
||||
|
||||
// Ctrl sequences
|
||||
{ name: "ctrl+c", data: "\x03", expected: "ctrl+c" },
|
||||
{ name: "ctrl+z", data: "\x1a", expected: "ctrl+z" },
|
||||
{ name: "ctrl+space", data: "\x00", expected: "ctrl+space" },
|
||||
|
||||
// Alt sequences (legacy mode)
|
||||
{ name: "alt+backspace", data: "\x1b\x7f", expected: "alt+backspace" },
|
||||
{ name: "alt+left", data: "\x1bb", expected: "alt+left" },
|
||||
{ name: "alt+right", data: "\x1bf", expected: "alt+right" },
|
||||
|
||||
// Arrow with modifiers (legacy)
|
||||
{ name: "shift+up", data: "\x1b[a", expected: "shift+up" },
|
||||
{ name: "ctrl+up", data: "\x1bOa", expected: "ctrl+up" },
|
||||
|
||||
// Printable characters
|
||||
{ name: "letter a", data: "a", expected: "a" },
|
||||
{ name: "letter z", data: "z", expected: "z" },
|
||||
{ name: "symbol /", data: "/", expected: "/" },
|
||||
];
|
||||
|
||||
function bench(name: string, fn: () => void): number {
|
||||
const start = performance.now();
|
||||
for (let i = 0; i < ITERATIONS; i++) {
|
||||
fn();
|
||||
}
|
||||
const elapsed = performance.now() - start;
|
||||
const perOp = (elapsed / ITERATIONS).toFixed(4);
|
||||
console.log(`${name}: ${elapsed.toFixed(2)}ms total (${perOp}ms/op)`);
|
||||
return elapsed;
|
||||
}
|
||||
|
||||
// Set to legacy mode for consistent comparison
|
||||
js.setKittyProtocolActive(true);
|
||||
native.setKittyProtocolActive(true);
|
||||
|
||||
console.log(`parseKey benchmark (${ITERATIONS} iterations, ${samples.length} samples each)\n`);
|
||||
|
||||
// Verify correctness first
|
||||
let mismatches = 0;
|
||||
for (const sample of samples) {
|
||||
const jsResult = js.parseKey(sample.data);
|
||||
const nativeResult = nativeParseKey(sample.data, false);
|
||||
if (jsResult !== nativeResult) {
|
||||
console.log(`MISMATCH ${sample.name}: js="${jsResult}" native="${nativeResult}" expected="${sample.expected}"`);
|
||||
mismatches++;
|
||||
}
|
||||
}
|
||||
if (mismatches > 0) {
|
||||
console.log(`\n${mismatches} mismatches found!\n`);
|
||||
} else {
|
||||
console.log("All results match.\n");
|
||||
}
|
||||
|
||||
const jsTime = bench("js/parseKey", () => {
|
||||
for (const sample of samples) {
|
||||
js.parseKey(sample.data);
|
||||
}
|
||||
});
|
||||
|
||||
const nativeTime = bench("native/parseKey", () => {
|
||||
for (const sample of samples) {
|
||||
native.parseKey(sample.data);
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`\nSpeedup: ${(jsTime / nativeTime).toFixed(2)}x`);
|
||||
|
||||
bench("js/parse+match", () => {
|
||||
for (const sample of samples) {
|
||||
js.matchesKey(sample.data, sample.expected as any);
|
||||
}
|
||||
});
|
||||
|
||||
bench("native/match", () => {
|
||||
for (const sample of samples) {
|
||||
native.matchesKey(sample.data, sample.expected as any);
|
||||
}
|
||||
});
|
||||
@@ -39,8 +39,6 @@ export {
|
||||
isKeyRelease,
|
||||
isKeyRepeat,
|
||||
isKittyProtocolActive,
|
||||
Key,
|
||||
type KeyEventType,
|
||||
type KeyId,
|
||||
matchesKey,
|
||||
parseKey,
|
||||
|
||||
+45
-910
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,11 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import { Key, matchesKey, setKittyProtocolActive } from "@oh-my-pi/pi-tui/keys";
|
||||
import { matchesKey, setKittyProtocolActive } from "@oh-my-pi/pi-tui/keys";
|
||||
|
||||
describe("matchesKey", () => {
|
||||
it("matches ctrl+letter sequences", () => {
|
||||
setKittyProtocolActive(false);
|
||||
const ctrlC = String.fromCharCode(3);
|
||||
expect(matchesKey(ctrlC, Key.ctrl("c"))).toBe(true);
|
||||
expect(matchesKey(ctrlC, "ctrl+c")).toBe(true);
|
||||
});
|
||||
|
||||
it("matches shifted tab", () => {
|
||||
|
||||
Reference in New Issue
Block a user