You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
49 lines
1.6 KiB
TypeScript
49 lines
1.6 KiB
TypeScript
/**
|
|
* Keyboard-wedge scanners type like superhuman typists: a burst of characters
|
|
* with tiny inter-key gaps, terminated by Enter. This detector distinguishes a
|
|
* scan from human typing purely from timing + length, with injected timestamps
|
|
* so it is testable without a DOM or a clock.
|
|
*/
|
|
export interface WedgeOptions {
|
|
/** Max gap between scanner keystrokes; humans are slower. */
|
|
maxInterKeyMs: number
|
|
/** Bursts shorter than this are treated as typed input. */
|
|
minLength: number
|
|
}
|
|
|
|
export const DEFAULT_WEDGE: WedgeOptions = { maxInterKeyMs: 35, minLength: 6 }
|
|
|
|
export type WedgeResult = { type: 'scan'; code: string } | { type: 'typed'; text: string }
|
|
|
|
export class WedgeDetector {
|
|
private chars: string[] = []
|
|
private lastAtMs: number | undefined
|
|
private maxGapMs = 0
|
|
|
|
constructor(private readonly opts: WedgeOptions = DEFAULT_WEDGE) {}
|
|
|
|
feed(char: string, atMs: number): void {
|
|
if (char.length !== 1) throw new Error('feed() takes single characters')
|
|
if (this.lastAtMs !== undefined) {
|
|
this.maxGapMs = Math.max(this.maxGapMs, atMs - this.lastAtMs)
|
|
}
|
|
this.lastAtMs = atMs
|
|
this.chars.push(char)
|
|
}
|
|
|
|
/** Call on Enter. Decides scan vs typed and resets for the next burst. */
|
|
terminate(): WedgeResult {
|
|
const text = this.chars.join('')
|
|
const isScan =
|
|
this.chars.length >= this.opts.minLength && this.maxGapMs <= this.opts.maxInterKeyMs
|
|
this.reset()
|
|
return isScan ? { type: 'scan', code: text } : { type: 'typed', text }
|
|
}
|
|
|
|
reset(): void {
|
|
this.chars = []
|
|
this.lastAtMs = undefined
|
|
this.maxGapMs = 0
|
|
}
|
|
}
|