mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-16 02:01:29 +00:00
fix(tui): handle Kitty protocol lock key modifiers
Fixes keyboard input in Ghostty on Linux when Num Lock is enabled. The Kitty protocol includes Caps Lock (64) and Num Lock (128) bits in modifier values. Now masks out lock bits when matching shortcuts. Added helper functions: isArrowUp/Down/Left/Right, isEnter, isTab, isBackspace, isShiftEnter, isAltEnter, isAltLeft/Right, isCtrlLeft/Right fixes #243
This commit is contained in:
parent
7e38897673
commit
28c3ffb914
6 changed files with 278 additions and 42 deletions
|
|
@ -10,6 +10,8 @@
|
|||
|
||||
- **Hooks wrap custom tools**: Custom tools are now executed through the hook wrapper, so `tool_call`/`tool_result` hooks can observe, block, and modify custom tool executions (consistent with hook type docs).
|
||||
|
||||
- **Kitty keyboard protocol on Linux**: Fixed keyboard input not working in Ghostty on Linux when Num Lock is enabled. The Kitty protocol includes Caps Lock and Num Lock state in modifier values, which broke key detection. Now correctly masks out lock key bits when matching keyboard shortcuts. ([#243](https://github.com/badlogic/pi-mono/issues/243))
|
||||
|
||||
## [0.24.0] - 2025-12-19
|
||||
|
||||
### Added
|
||||
|
|
|
|||
|
|
@ -1,5 +1,26 @@
|
|||
import type { AutocompleteProvider, CombinedAutocompleteProvider } from "../autocomplete.js";
|
||||
import { isAltBackspace, isCtrlA, isCtrlC, isCtrlE, isCtrlK, isCtrlU, isCtrlW, isEscape, Keys } from "../keys.js";
|
||||
import {
|
||||
isAltBackspace,
|
||||
isAltEnter,
|
||||
isAltLeft,
|
||||
isAltRight,
|
||||
isArrowDown,
|
||||
isArrowLeft,
|
||||
isArrowRight,
|
||||
isArrowUp,
|
||||
isCtrlA,
|
||||
isCtrlC,
|
||||
isCtrlE,
|
||||
isCtrlK,
|
||||
isCtrlLeft,
|
||||
isCtrlRight,
|
||||
isCtrlU,
|
||||
isCtrlW,
|
||||
isEnter,
|
||||
isEscape,
|
||||
isShiftEnter,
|
||||
isTab,
|
||||
} from "../keys.js";
|
||||
import type { Component } from "../tui.js";
|
||||
import { visibleWidth } from "../utils.js";
|
||||
import { SelectList, type SelectListTheme } from "./select-list.js";
|
||||
|
|
@ -272,15 +293,15 @@ export class Editor implements Component {
|
|||
return;
|
||||
}
|
||||
// Let the autocomplete list handle navigation and selection
|
||||
else if (data === "\x1b[A" || data === "\x1b[B" || data === "\r" || data === "\t") {
|
||||
else if (isArrowUp(data) || isArrowDown(data) || isEnter(data) || isTab(data)) {
|
||||
// Only pass arrow keys to the list, not Enter/Tab (we handle those directly)
|
||||
if (data === "\x1b[A" || data === "\x1b[B") {
|
||||
if (isArrowUp(data) || isArrowDown(data)) {
|
||||
this.autocompleteList.handleInput(data);
|
||||
return;
|
||||
}
|
||||
|
||||
// If Tab was pressed, always apply the selection
|
||||
if (data === "\t") {
|
||||
if (isTab(data)) {
|
||||
const selected = this.autocompleteList.getSelectedItem();
|
||||
if (selected && this.autocompleteProvider) {
|
||||
const result = this.autocompleteProvider.applyCompletion(
|
||||
|
|
@ -305,7 +326,7 @@ export class Editor implements Component {
|
|||
}
|
||||
|
||||
// If Enter was pressed on a slash command, apply completion and submit
|
||||
if (data === "\r" && this.autocompletePrefix.startsWith("/")) {
|
||||
if (isEnter(data) && this.autocompletePrefix.startsWith("/")) {
|
||||
const selected = this.autocompleteList.getSelectedItem();
|
||||
if (selected && this.autocompleteProvider) {
|
||||
const result = this.autocompleteProvider.applyCompletion(
|
||||
|
|
@ -324,7 +345,7 @@ export class Editor implements Component {
|
|||
// Don't return - fall through to submission logic
|
||||
}
|
||||
// If Enter was pressed on a file path, apply completion
|
||||
else if (data === "\r") {
|
||||
else if (isEnter(data)) {
|
||||
const selected = this.autocompleteList.getSelectedItem();
|
||||
if (selected && this.autocompleteProvider) {
|
||||
const result = this.autocompleteProvider.applyCompletion(
|
||||
|
|
@ -353,7 +374,7 @@ export class Editor implements Component {
|
|||
}
|
||||
|
||||
// Tab key - context-aware completion (but not when already autocompleting)
|
||||
if (data === "\t" && !this.isAutocompleting) {
|
||||
if (isTab(data) && !this.isAutocompleting) {
|
||||
this.handleTabCompletion();
|
||||
return;
|
||||
}
|
||||
|
|
@ -388,8 +409,8 @@ export class Editor implements Component {
|
|||
(data.charCodeAt(0) === 10 && data.length > 1) || // Ctrl+Enter with modifiers
|
||||
data === "\x1b\r" || // Option+Enter in some terminals (legacy)
|
||||
data === "\x1b[13;2~" || // Shift+Enter in some terminals (legacy format)
|
||||
data === Keys.SHIFT_ENTER || // Shift+Enter in Kitty keyboard protocol
|
||||
data === Keys.ALT_ENTER || // Alt+Enter in Kitty keyboard protocol
|
||||
isShiftEnter(data) || // Shift+Enter (Kitty protocol, handles lock bits)
|
||||
isAltEnter(data) || // Alt+Enter (Kitty protocol, handles lock bits)
|
||||
(data.length > 1 && data.includes("\x1b") && data.includes("\r")) ||
|
||||
(data === "\n" && data.length === 1) || // Shift+Enter from iTerm2 mapping
|
||||
data === "\\\r" // Shift+Enter in VS Code terminal
|
||||
|
|
@ -451,19 +472,15 @@ export class Editor implements Component {
|
|||
this.handleForwardDelete();
|
||||
}
|
||||
// Word navigation (Option/Alt + Arrow or Ctrl + Arrow)
|
||||
// Option+Left: \x1b[1;3D or \x1bb
|
||||
// Option+Right: \x1b[1;3C or \x1bf
|
||||
// Ctrl+Left: \x1b[1;5D
|
||||
// Ctrl+Right: \x1b[1;5C
|
||||
else if (data === "\x1b[1;3D" || data === "\x1bb" || data === "\x1b[1;5D") {
|
||||
else if (isAltLeft(data) || isCtrlLeft(data)) {
|
||||
// Word left
|
||||
this.moveWordBackwards();
|
||||
} else if (data === "\x1b[1;3C" || data === "\x1bf" || data === "\x1b[1;5C") {
|
||||
} else if (isAltRight(data) || isCtrlRight(data)) {
|
||||
// Word right
|
||||
this.moveWordForwards();
|
||||
}
|
||||
// Arrow keys
|
||||
else if (data === "\x1b[A") {
|
||||
else if (isArrowUp(data)) {
|
||||
// Up - history navigation or cursor movement
|
||||
if (this.isEditorEmpty()) {
|
||||
this.navigateHistory(-1); // Start browsing history
|
||||
|
|
@ -472,17 +489,17 @@ export class Editor implements Component {
|
|||
} else {
|
||||
this.moveCursor(-1, 0); // Cursor movement (within text or history entry)
|
||||
}
|
||||
} else if (data === "\x1b[B") {
|
||||
} else if (isArrowDown(data)) {
|
||||
// Down - history navigation or cursor movement
|
||||
if (this.historyIndex > -1 && this.isOnLastVisualLine()) {
|
||||
this.navigateHistory(1); // Navigate to newer history entry or clear
|
||||
} else {
|
||||
this.moveCursor(1, 0); // Cursor movement (within text or history entry)
|
||||
}
|
||||
} else if (data === "\x1b[C") {
|
||||
} else if (isArrowRight(data)) {
|
||||
// Right
|
||||
this.moveCursor(0, 1);
|
||||
} else if (data === "\x1b[D") {
|
||||
} else if (isArrowLeft(data)) {
|
||||
// Left
|
||||
this.moveCursor(0, -1);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { isAltBackspace, isCtrlA, isCtrlE, isCtrlK, isCtrlU, isCtrlW } from "../keys.js";
|
||||
import { isAltBackspace, isArrowLeft, isArrowRight, isCtrlA, isCtrlE, isCtrlK, isCtrlU, isCtrlW } from "../keys.js";
|
||||
import type { Component } from "../tui.js";
|
||||
import { visibleWidth } from "../utils.js";
|
||||
|
||||
|
|
@ -78,7 +78,7 @@ export class Input implements Component {
|
|||
return;
|
||||
}
|
||||
|
||||
if (data === "\x1b[D") {
|
||||
if (isArrowLeft(data)) {
|
||||
// Left arrow
|
||||
if (this.cursor > 0) {
|
||||
this.cursor--;
|
||||
|
|
@ -86,7 +86,7 @@ export class Input implements Component {
|
|||
return;
|
||||
}
|
||||
|
||||
if (data === "\x1b[C") {
|
||||
if (isArrowRight(data)) {
|
||||
// Right arrow
|
||||
if (this.cursor < this.value.length) {
|
||||
this.cursor++;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { isCtrlC, isEscape } from "../keys.js";
|
||||
import { isArrowDown, isArrowUp, isCtrlC, isEnter, isEscape } from "../keys.js";
|
||||
import type { Component } from "../tui.js";
|
||||
import { truncateToWidth } from "../utils.js";
|
||||
|
||||
|
|
@ -146,17 +146,17 @@ export class SelectList implements Component {
|
|||
|
||||
handleInput(keyData: string): void {
|
||||
// Up arrow - wrap to bottom when at top
|
||||
if (keyData === "\x1b[A") {
|
||||
if (isArrowUp(keyData)) {
|
||||
this.selectedIndex = this.selectedIndex === 0 ? this.filteredItems.length - 1 : this.selectedIndex - 1;
|
||||
this.notifySelectionChange();
|
||||
}
|
||||
// Down arrow - wrap to top when at bottom
|
||||
else if (keyData === "\x1b[B") {
|
||||
else if (isArrowDown(keyData)) {
|
||||
this.selectedIndex = this.selectedIndex === this.filteredItems.length - 1 ? 0 : this.selectedIndex + 1;
|
||||
this.notifySelectionChange();
|
||||
}
|
||||
// Enter
|
||||
else if (keyData === "\r") {
|
||||
else if (isEnter(keyData)) {
|
||||
const selectedItem = this.filteredItems[this.selectedIndex];
|
||||
if (selectedItem && this.onSelect) {
|
||||
this.onSelect(selectedItem);
|
||||
|
|
|
|||
|
|
@ -21,18 +21,31 @@ export { TruncatedText } from "./components/truncated-text.js";
|
|||
// Kitty keyboard protocol helpers
|
||||
export {
|
||||
isAltBackspace,
|
||||
isAltEnter,
|
||||
isAltLeft,
|
||||
isAltRight,
|
||||
isArrowDown,
|
||||
isArrowLeft,
|
||||
isArrowRight,
|
||||
isArrowUp,
|
||||
isBackspace,
|
||||
isCtrlA,
|
||||
isCtrlC,
|
||||
isCtrlD,
|
||||
isCtrlE,
|
||||
isCtrlK,
|
||||
isCtrlLeft,
|
||||
isCtrlO,
|
||||
isCtrlP,
|
||||
isCtrlRight,
|
||||
isCtrlT,
|
||||
isCtrlU,
|
||||
isCtrlW,
|
||||
isEnter,
|
||||
isEscape,
|
||||
isShiftEnter,
|
||||
isShiftTab,
|
||||
isTab,
|
||||
Keys,
|
||||
} from "./keys.js";
|
||||
// Terminal interface and implementations
|
||||
|
|
|
|||
|
|
@ -4,13 +4,21 @@
|
|||
* The Kitty keyboard protocol sends enhanced escape sequences in the format:
|
||||
* \x1b[<codepoint>;<modifier>u
|
||||
*
|
||||
* Modifier values (added to 1):
|
||||
* Modifier bits (before adding 1 for transmission):
|
||||
* - Shift: 1 (value 2)
|
||||
* - Alt: 2 (value 3)
|
||||
* - Ctrl: 4 (value 5)
|
||||
* - Super: 8 (value 9)
|
||||
* - Hyper: 16
|
||||
* - Meta: 32
|
||||
* - Caps_Lock: 64
|
||||
* - Num_Lock: 128
|
||||
*
|
||||
* See: https://sw.kovidgoyal.net/kitty/keyboard-protocol/
|
||||
*
|
||||
* NOTE: Some terminals (e.g., Ghostty on Linux) include lock key states
|
||||
* (Caps Lock, Num Lock) in the modifier field. We mask these out when
|
||||
* checking for key combinations since they shouldn't affect behavior.
|
||||
*/
|
||||
|
||||
// Common codepoints
|
||||
|
|
@ -34,6 +42,9 @@ const CODEPOINTS = {
|
|||
backspace: 127,
|
||||
} as const;
|
||||
|
||||
// Lock key bits to ignore when matching (Caps Lock + Num Lock)
|
||||
const LOCK_MASK = 64 + 128; // 192
|
||||
|
||||
// Modifier bits (before adding 1)
|
||||
const MODIFIERS = {
|
||||
shift: 1,
|
||||
|
|
@ -49,6 +60,60 @@ function kittySequence(codepoint: number, modifier: number): string {
|
|||
return `\x1b[${codepoint};${modifier + 1}u`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parsed Kitty keyboard protocol sequence.
|
||||
*/
|
||||
interface ParsedKittySequence {
|
||||
codepoint: number;
|
||||
modifier: number; // Actual modifier bits (after subtracting 1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a Kitty keyboard protocol sequence.
|
||||
* Handles formats:
|
||||
* - \x1b[<codepoint>u (no modifier)
|
||||
* - \x1b[<codepoint>;<modifier>u (with modifier)
|
||||
* - \x1b[1;<modifier>A/B/C/D (arrow keys with modifier)
|
||||
*
|
||||
* Returns null if not a valid Kitty sequence.
|
||||
*/
|
||||
function parseKittySequence(data: string): ParsedKittySequence | null {
|
||||
// Match CSI u format: \x1b[<num>u or \x1b[<num>;<mod>u
|
||||
const csiUMatch = data.match(/^\x1b\[(\d+)(?:;(\d+))?u$/);
|
||||
if (csiUMatch) {
|
||||
const codepoint = parseInt(csiUMatch[1]!, 10);
|
||||
const modValue = csiUMatch[2] ? parseInt(csiUMatch[2], 10) : 1;
|
||||
return { codepoint, modifier: modValue - 1 };
|
||||
}
|
||||
|
||||
// Match arrow keys with modifier: \x1b[1;<mod>A/B/C/D
|
||||
const arrowMatch = data.match(/^\x1b\[1;(\d+)([ABCD])$/);
|
||||
if (arrowMatch) {
|
||||
const modValue = parseInt(arrowMatch[1]!, 10);
|
||||
// Map arrow letters to virtual codepoints for easier matching
|
||||
const arrowCodes: Record<string, number> = { A: -1, B: -2, C: -3, D: -4 };
|
||||
const codepoint = arrowCodes[arrowMatch[2]!]!;
|
||||
return { codepoint, modifier: modValue - 1 };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a Kitty sequence matches the expected codepoint and modifier,
|
||||
* ignoring lock key bits (Caps Lock, Num Lock).
|
||||
*/
|
||||
function matchesKittySequence(data: string, expectedCodepoint: number, expectedModifier: number): boolean {
|
||||
const parsed = parseKittySequence(data);
|
||||
if (!parsed) return false;
|
||||
|
||||
// Mask out lock bits from both sides for comparison
|
||||
const actualMod = parsed.modifier & ~LOCK_MASK;
|
||||
const expectedMod = expectedModifier & ~LOCK_MASK;
|
||||
|
||||
return parsed.codepoint === expectedCodepoint && actualMod === expectedMod;
|
||||
}
|
||||
|
||||
// Pre-built sequences for common key combinations
|
||||
export const Keys = {
|
||||
// Ctrl+<letter> combinations
|
||||
|
|
@ -77,23 +142,31 @@ export const Keys = {
|
|||
|
||||
/**
|
||||
* Check if input matches a Kitty protocol Ctrl+<key> sequence.
|
||||
* Ignores lock key bits (Caps Lock, Num Lock).
|
||||
* @param data - The input data to check
|
||||
* @param key - Single lowercase letter (e.g., 'c' for Ctrl+C)
|
||||
*/
|
||||
export function isKittyCtrl(data: string, key: string): boolean {
|
||||
if (key.length !== 1) return false;
|
||||
const codepoint = key.charCodeAt(0);
|
||||
return data === kittySequence(codepoint, MODIFIERS.ctrl);
|
||||
// Check exact match first (fast path)
|
||||
if (data === kittySequence(codepoint, MODIFIERS.ctrl)) return true;
|
||||
// Check with lock bits masked out
|
||||
return matchesKittySequence(data, codepoint, MODIFIERS.ctrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if input matches a Kitty protocol key sequence with specific modifier.
|
||||
* Ignores lock key bits (Caps Lock, Num Lock).
|
||||
* @param data - The input data to check
|
||||
* @param codepoint - ASCII codepoint of the key
|
||||
* @param modifier - Modifier value (use MODIFIERS constants)
|
||||
*/
|
||||
export function isKittyKey(data: string, codepoint: number, modifier: number): boolean {
|
||||
return data === kittySequence(codepoint, modifier);
|
||||
// Check exact match first (fast path)
|
||||
if (data === kittySequence(codepoint, modifier)) return true;
|
||||
// Check with lock bits masked out
|
||||
return matchesKittySequence(data, codepoint, modifier);
|
||||
}
|
||||
|
||||
// Raw control character codes
|
||||
|
|
@ -114,93 +187,224 @@ const RAW = {
|
|||
|
||||
/**
|
||||
* Check if input matches Ctrl+A (raw byte or Kitty protocol).
|
||||
* Ignores lock key bits.
|
||||
*/
|
||||
export function isCtrlA(data: string): boolean {
|
||||
return data === RAW.CTRL_A || data === Keys.CTRL_A;
|
||||
return data === RAW.CTRL_A || data === Keys.CTRL_A || matchesKittySequence(data, CODEPOINTS.a, MODIFIERS.ctrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if input matches Ctrl+C (raw byte or Kitty protocol).
|
||||
* Ignores lock key bits.
|
||||
*/
|
||||
export function isCtrlC(data: string): boolean {
|
||||
return data === RAW.CTRL_C || data === Keys.CTRL_C;
|
||||
return data === RAW.CTRL_C || data === Keys.CTRL_C || matchesKittySequence(data, CODEPOINTS.c, MODIFIERS.ctrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if input matches Ctrl+D (raw byte or Kitty protocol).
|
||||
* Ignores lock key bits.
|
||||
*/
|
||||
export function isCtrlD(data: string): boolean {
|
||||
return data === RAW.CTRL_D || data === Keys.CTRL_D;
|
||||
return data === RAW.CTRL_D || data === Keys.CTRL_D || matchesKittySequence(data, CODEPOINTS.d, MODIFIERS.ctrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if input matches Ctrl+E (raw byte or Kitty protocol).
|
||||
* Ignores lock key bits.
|
||||
*/
|
||||
export function isCtrlE(data: string): boolean {
|
||||
return data === RAW.CTRL_E || data === Keys.CTRL_E;
|
||||
return data === RAW.CTRL_E || data === Keys.CTRL_E || matchesKittySequence(data, CODEPOINTS.e, MODIFIERS.ctrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if input matches Ctrl+K (raw byte or Kitty protocol).
|
||||
* Ignores lock key bits.
|
||||
*/
|
||||
export function isCtrlK(data: string): boolean {
|
||||
return data === RAW.CTRL_K || data === Keys.CTRL_K;
|
||||
return data === RAW.CTRL_K || data === Keys.CTRL_K || matchesKittySequence(data, CODEPOINTS.k, MODIFIERS.ctrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if input matches Ctrl+O (raw byte or Kitty protocol).
|
||||
* Ignores lock key bits.
|
||||
*/
|
||||
export function isCtrlO(data: string): boolean {
|
||||
return data === RAW.CTRL_O || data === Keys.CTRL_O;
|
||||
return data === RAW.CTRL_O || data === Keys.CTRL_O || matchesKittySequence(data, CODEPOINTS.o, MODIFIERS.ctrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if input matches Ctrl+P (raw byte or Kitty protocol).
|
||||
* Ignores lock key bits.
|
||||
*/
|
||||
export function isCtrlP(data: string): boolean {
|
||||
return data === RAW.CTRL_P || data === Keys.CTRL_P;
|
||||
return data === RAW.CTRL_P || data === Keys.CTRL_P || matchesKittySequence(data, CODEPOINTS.p, MODIFIERS.ctrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if input matches Ctrl+T (raw byte or Kitty protocol).
|
||||
* Ignores lock key bits.
|
||||
*/
|
||||
export function isCtrlT(data: string): boolean {
|
||||
return data === RAW.CTRL_T || data === Keys.CTRL_T;
|
||||
return data === RAW.CTRL_T || data === Keys.CTRL_T || matchesKittySequence(data, CODEPOINTS.t, MODIFIERS.ctrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if input matches Ctrl+U (raw byte or Kitty protocol).
|
||||
* Ignores lock key bits.
|
||||
*/
|
||||
export function isCtrlU(data: string): boolean {
|
||||
return data === RAW.CTRL_U || data === Keys.CTRL_U;
|
||||
return data === RAW.CTRL_U || data === Keys.CTRL_U || matchesKittySequence(data, CODEPOINTS.u, MODIFIERS.ctrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if input matches Ctrl+W (raw byte or Kitty protocol).
|
||||
* Ignores lock key bits.
|
||||
*/
|
||||
export function isCtrlW(data: string): boolean {
|
||||
return data === RAW.CTRL_W || data === Keys.CTRL_W;
|
||||
return data === RAW.CTRL_W || data === Keys.CTRL_W || matchesKittySequence(data, CODEPOINTS.w, MODIFIERS.ctrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if input matches Alt+Backspace (legacy or Kitty protocol).
|
||||
* Ignores lock key bits.
|
||||
*/
|
||||
export function isAltBackspace(data: string): boolean {
|
||||
return data === RAW.ALT_BACKSPACE || data === Keys.ALT_BACKSPACE;
|
||||
return (
|
||||
data === RAW.ALT_BACKSPACE ||
|
||||
data === Keys.ALT_BACKSPACE ||
|
||||
matchesKittySequence(data, CODEPOINTS.backspace, MODIFIERS.alt)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if input matches Shift+Tab (legacy or Kitty protocol).
|
||||
* Ignores lock key bits.
|
||||
*/
|
||||
export function isShiftTab(data: string): boolean {
|
||||
return data === RAW.SHIFT_TAB || data === Keys.SHIFT_TAB;
|
||||
return (
|
||||
data === RAW.SHIFT_TAB || data === Keys.SHIFT_TAB || matchesKittySequence(data, CODEPOINTS.tab, MODIFIERS.shift)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if input matches the Escape key (raw byte or Kitty protocol).
|
||||
* Raw: \x1b (single byte)
|
||||
* Kitty: \x1b[27u (codepoint 27 = escape)
|
||||
* Ignores lock key bits.
|
||||
*/
|
||||
export function isEscape(data: string): boolean {
|
||||
return data === "\x1b" || data === `\x1b[${CODEPOINTS.escape}u`;
|
||||
return data === "\x1b" || data === `\x1b[${CODEPOINTS.escape}u` || matchesKittySequence(data, CODEPOINTS.escape, 0);
|
||||
}
|
||||
|
||||
// Arrow key virtual codepoints (negative to avoid conflicts with real codepoints)
|
||||
const ARROW_CODEPOINTS = {
|
||||
up: -1,
|
||||
down: -2,
|
||||
right: -3,
|
||||
left: -4,
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Check if input matches Arrow Up key.
|
||||
* Handles both legacy (\x1b[A) and Kitty protocol with modifiers.
|
||||
*/
|
||||
export function isArrowUp(data: string): boolean {
|
||||
return data === "\x1b[A" || matchesKittySequence(data, ARROW_CODEPOINTS.up, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if input matches Arrow Down key.
|
||||
* Handles both legacy (\x1b[B) and Kitty protocol with modifiers.
|
||||
*/
|
||||
export function isArrowDown(data: string): boolean {
|
||||
return data === "\x1b[B" || matchesKittySequence(data, ARROW_CODEPOINTS.down, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if input matches Arrow Right key.
|
||||
* Handles both legacy (\x1b[C) and Kitty protocol with modifiers.
|
||||
*/
|
||||
export function isArrowRight(data: string): boolean {
|
||||
return data === "\x1b[C" || matchesKittySequence(data, ARROW_CODEPOINTS.right, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if input matches Arrow Left key.
|
||||
* Handles both legacy (\x1b[D) and Kitty protocol with modifiers.
|
||||
*/
|
||||
export function isArrowLeft(data: string): boolean {
|
||||
return data === "\x1b[D" || matchesKittySequence(data, ARROW_CODEPOINTS.left, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if input matches plain Tab key (no modifiers).
|
||||
* Handles both legacy (\t) and Kitty protocol.
|
||||
*/
|
||||
export function isTab(data: string): boolean {
|
||||
return data === "\t" || matchesKittySequence(data, CODEPOINTS.tab, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if input matches plain Enter/Return key (no modifiers).
|
||||
* Handles both legacy (\r) and Kitty protocol.
|
||||
*/
|
||||
export function isEnter(data: string): boolean {
|
||||
return data === "\r" || matchesKittySequence(data, CODEPOINTS.enter, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if input matches plain Backspace key (no modifiers).
|
||||
* Handles both legacy (\x7f, \x08) and Kitty protocol.
|
||||
*/
|
||||
export function isBackspace(data: string): boolean {
|
||||
return data === "\x7f" || data === "\x08" || matchesKittySequence(data, CODEPOINTS.backspace, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if input matches Shift+Enter.
|
||||
* Ignores lock key bits.
|
||||
*/
|
||||
export function isShiftEnter(data: string): boolean {
|
||||
return data === Keys.SHIFT_ENTER || matchesKittySequence(data, CODEPOINTS.enter, MODIFIERS.shift);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if input matches Alt+Enter.
|
||||
* Ignores lock key bits.
|
||||
*/
|
||||
export function isAltEnter(data: string): boolean {
|
||||
return data === Keys.ALT_ENTER || data === "\x1b\r" || matchesKittySequence(data, CODEPOINTS.enter, MODIFIERS.alt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if input matches Option/Alt+Left (word navigation).
|
||||
* Handles multiple formats including Kitty protocol.
|
||||
*/
|
||||
export function isAltLeft(data: string): boolean {
|
||||
return data === "\x1b[1;3D" || data === "\x1bb" || matchesKittySequence(data, ARROW_CODEPOINTS.left, MODIFIERS.alt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if input matches Option/Alt+Right (word navigation).
|
||||
* Handles multiple formats including Kitty protocol.
|
||||
*/
|
||||
export function isAltRight(data: string): boolean {
|
||||
return data === "\x1b[1;3C" || data === "\x1bf" || matchesKittySequence(data, ARROW_CODEPOINTS.right, MODIFIERS.alt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if input matches Ctrl+Left (word navigation).
|
||||
* Handles multiple formats including Kitty protocol.
|
||||
*/
|
||||
export function isCtrlLeft(data: string): boolean {
|
||||
return data === "\x1b[1;5D" || matchesKittySequence(data, ARROW_CODEPOINTS.left, MODIFIERS.ctrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if input matches Ctrl+Right (word navigation).
|
||||
* Handles multiple formats including Kitty protocol.
|
||||
*/
|
||||
export function isCtrlRight(data: string): boolean {
|
||||
return data === "\x1b[1;5C" || matchesKittySequence(data, ARROW_CODEPOINTS.right, MODIFIERS.ctrl);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue