// SPDX-License-Identifier: AGPL-3.0-or-later import {PUNCTUATION_KEY_CODES} from '@fluxer/desktop_ipc/src/GlobalShortcutKeyNames'; const EVDEV_KEY_CODES: ReadonlyMap = new Map([ [0x001, 'Escape'], [0x002, 'Digit1'], [0x003, 'Digit2'], [0x004, 'Digit3'], [0x005, 'Digit4'], [0x006, 'Digit5'], [0x007, 'Digit6'], [0x008, 'Digit7'], [0x009, 'Digit8'], [0x00a, 'Digit9'], [0x00b, 'Digit0'], [0x00c, 'Minus'], [0x00d, 'Equal'], [0x00e, 'Backspace'], [0x00f, 'Tab'], [0x010, 'KeyQ'], [0x011, 'KeyW'], [0x012, 'KeyE'], [0x013, 'KeyR'], [0x014, 'KeyT'], [0x015, 'KeyY'], [0x016, 'KeyU'], [0x017, 'KeyI'], [0x018, 'KeyO'], [0x019, 'KeyP'], [0x01a, 'BracketLeft'], [0x01b, 'BracketRight'], [0x01c, 'Enter'], [0x01d, 'ControlLeft'], [0x01e, 'KeyA'], [0x01f, 'KeyS'], [0x020, 'KeyD'], [0x021, 'KeyF'], [0x022, 'KeyG'], [0x023, 'KeyH'], [0x024, 'KeyJ'], [0x025, 'KeyK'], [0x026, 'KeyL'], [0x027, 'Semicolon'], [0x028, 'Quote'], [0x029, 'Backquote'], [0x02a, 'ShiftLeft'], [0x02b, 'Backslash'], [0x02c, 'KeyZ'], [0x02d, 'KeyX'], [0x02e, 'KeyC'], [0x02f, 'KeyV'], [0x030, 'KeyB'], [0x031, 'KeyN'], [0x032, 'KeyM'], [0x033, 'Comma'], [0x034, 'Period'], [0x035, 'Slash'], [0x036, 'ShiftRight'], [0x037, 'NumpadMultiply'], [0x038, 'AltLeft'], [0x039, 'Space'], [0x03a, 'CapsLock'], [0x03b, 'F1'], [0x03c, 'F2'], [0x03d, 'F3'], [0x03e, 'F4'], [0x03f, 'F5'], [0x040, 'F6'], [0x041, 'F7'], [0x042, 'F8'], [0x043, 'F9'], [0x044, 'F10'], [0x045, 'NumLock'], [0x046, 'ScrollLock'], [0x047, 'Numpad7'], [0x048, 'Numpad8'], [0x049, 'Numpad9'], [0x04a, 'NumpadSubtract'], [0x04b, 'Numpad4'], [0x04c, 'Numpad5'], [0x04d, 'Numpad6'], [0x04e, 'NumpadAdd'], [0x04f, 'Numpad1'], [0x050, 'Numpad2'], [0x051, 'Numpad3'], [0x052, 'Numpad0'], [0x053, 'NumpadDecimal'], [0x055, 'Lang5'], [0x056, 'IntlBackslash'], [0x057, 'F11'], [0x058, 'F12'], [0x059, 'IntlRo'], [0x05a, 'Lang3'], [0x05b, 'Lang4'], [0x05c, 'Convert'], [0x05d, 'KanaMode'], [0x05e, 'NonConvert'], [0x060, 'NumpadEnter'], [0x061, 'ControlRight'], [0x062, 'NumpadDivide'], [0x063, 'PrintScreen'], [0x064, 'AltRight'], [0x066, 'Home'], [0x067, 'ArrowUp'], [0x068, 'PageUp'], [0x069, 'ArrowLeft'], [0x06a, 'ArrowRight'], [0x06b, 'End'], [0x06c, 'ArrowDown'], [0x06d, 'PageDown'], [0x06e, 'Insert'], [0x06f, 'Delete'], [0x071, 'AudioVolumeMute'], [0x072, 'AudioVolumeDown'], [0x073, 'AudioVolumeUp'], [0x074, 'Power'], [0x075, 'NumpadEqual'], [0x077, 'Pause'], [0x078, 'ShowAllWindows'], [0x079, 'NumpadComma'], [0x07a, 'Lang1'], [0x07b, 'Lang2'], [0x07c, 'IntlYen'], [0x07d, 'MetaLeft'], [0x07e, 'MetaRight'], [0x07f, 'ContextMenu'], [0x080, 'BrowserStop'], [0x081, 'Again'], [0x083, 'Undo'], [0x084, 'Select'], [0x085, 'Copy'], [0x086, 'Open'], [0x087, 'Paste'], [0x088, 'Find'], [0x089, 'Cut'], [0x08a, 'Help'], [0x08c, 'LaunchApp2'], [0x08e, 'Sleep'], [0x08f, 'WakeUp'], [0x090, 'LaunchApp1'], [0x094, 'LaunchApp1'], [0x095, 'LaunchApp2'], [0x09b, 'LaunchMail'], [0x09c, 'BrowserFavorites'], [0x09e, 'BrowserBack'], [0x09f, 'BrowserForward'], [0x0a1, 'Eject'], [0x0a3, 'MediaTrackNext'], [0x0a4, 'MediaPlayPause'], [0x0a5, 'MediaTrackPrevious'], [0x0a6, 'MediaStop'], [0x0a7, 'MediaRecord'], [0x0a8, 'MediaRewind'], [0x0ab, 'MediaSelect'], [0x0ac, 'BrowserHome'], [0x0ad, 'BrowserRefresh'], [0x0b3, 'NumpadParenLeft'], [0x0b4, 'NumpadParenRight'], [0x0b7, 'F13'], [0x0b8, 'F14'], [0x0b9, 'F15'], [0x0ba, 'F16'], [0x0bb, 'F17'], [0x0bc, 'F18'], [0x0bd, 'F19'], [0x0be, 'F20'], [0x0bf, 'F21'], [0x0c0, 'F22'], [0x0c1, 'F23'], [0x0c2, 'F24'], [0x0c9, 'MediaPause'], [0x0cf, 'MediaPlay'], [0x0d0, 'MediaFastForward'], [0x0d9, 'BrowserSearch'], [0x0e0, 'BrightnessDown'], [0x0e1, 'BrightnessUp'], [0x0e2, 'MediaSelect'], [0x0e3, 'DisplayToggleIntExt'], [0x0e7, 'MailSend'], [0x0e8, 'MailReply'], [0x0e9, 'MailForward'], [0x16c, 'BrowserFavorites'], [0x174, 'ZoomToggle'], [0x1d0, 'Fn'], [0x243, 'LaunchControlPanel'], [0x244, 'SelectTask'], [0x245, 'LaunchScreenSaver'], [0x247, 'LaunchAssistant'], [0x248, 'KeyboardLayoutSelect'], ]); const WINDOWS_SCAN_CODES: ReadonlyMap = new Map([ [0x0001, 'Escape'], [0x0002, 'Digit1'], [0x0003, 'Digit2'], [0x0004, 'Digit3'], [0x0005, 'Digit4'], [0x0006, 'Digit5'], [0x0007, 'Digit6'], [0x0008, 'Digit7'], [0x0009, 'Digit8'], [0x000a, 'Digit9'], [0x000b, 'Digit0'], [0x000c, 'Minus'], [0x000d, 'Equal'], [0x000e, 'Backspace'], [0x000f, 'Tab'], [0x0010, 'KeyQ'], [0x0011, 'KeyW'], [0x0012, 'KeyE'], [0x0013, 'KeyR'], [0x0014, 'KeyT'], [0x0015, 'KeyY'], [0x0016, 'KeyU'], [0x0017, 'KeyI'], [0x0018, 'KeyO'], [0x0019, 'KeyP'], [0x001a, 'BracketLeft'], [0x001b, 'BracketRight'], [0x001c, 'Enter'], [0x001d, 'ControlLeft'], [0x001e, 'KeyA'], [0x001f, 'KeyS'], [0x0020, 'KeyD'], [0x0021, 'KeyF'], [0x0022, 'KeyG'], [0x0023, 'KeyH'], [0x0024, 'KeyJ'], [0x0025, 'KeyK'], [0x0026, 'KeyL'], [0x0027, 'Semicolon'], [0x0028, 'Quote'], [0x0029, 'Backquote'], [0x002a, 'ShiftLeft'], [0x002b, 'Backslash'], [0x002c, 'KeyZ'], [0x002d, 'KeyX'], [0x002e, 'KeyC'], [0x002f, 'KeyV'], [0x0030, 'KeyB'], [0x0031, 'KeyN'], [0x0032, 'KeyM'], [0x0033, 'Comma'], [0x0034, 'Period'], [0x0035, 'Slash'], [0x0036, 'ShiftRight'], [0x0037, 'NumpadMultiply'], [0x0038, 'AltLeft'], [0x0039, 'Space'], [0x003a, 'CapsLock'], [0x003b, 'F1'], [0x003c, 'F2'], [0x003d, 'F3'], [0x003e, 'F4'], [0x003f, 'F5'], [0x0040, 'F6'], [0x0041, 'F7'], [0x0042, 'F8'], [0x0043, 'F9'], [0x0044, 'F10'], [0x0045, 'Pause'], [0x0046, 'ScrollLock'], [0x0047, 'Numpad7'], [0x0048, 'Numpad8'], [0x0049, 'Numpad9'], [0x004a, 'NumpadSubtract'], [0x004b, 'Numpad4'], [0x004c, 'Numpad5'], [0x004d, 'Numpad6'], [0x004e, 'NumpadAdd'], [0x004f, 'Numpad1'], [0x0050, 'Numpad2'], [0x0051, 'Numpad3'], [0x0052, 'Numpad0'], [0x0053, 'NumpadDecimal'], [0x0054, 'PrintScreen'], [0x0056, 'IntlBackslash'], [0x0057, 'F11'], [0x0058, 'F12'], [0x0059, 'NumpadEqual'], [0x0064, 'F13'], [0x0065, 'F14'], [0x0066, 'F15'], [0x0067, 'F16'], [0x0068, 'F17'], [0x0069, 'F18'], [0x006a, 'F19'], [0x006b, 'F20'], [0x006c, 'F21'], [0x006d, 'F22'], [0x006e, 'F23'], [0x0070, 'KanaMode'], [0x0071, 'Lang2'], [0x0072, 'Lang1'], [0x0073, 'IntlRo'], [0x0076, 'F24'], [0x0077, 'Lang4'], [0x0078, 'Lang3'], [0x0079, 'Convert'], [0x007b, 'NonConvert'], [0x007d, 'IntlYen'], [0x007e, 'NumpadComma'], [0xe008, 'Undo'], [0xe00a, 'Paste'], [0xe010, 'MediaTrackPrevious'], [0xe017, 'Cut'], [0xe018, 'Copy'], [0xe019, 'MediaTrackNext'], [0xe01c, 'NumpadEnter'], [0xe01d, 'ControlRight'], [0xe020, 'AudioVolumeMute'], [0xe021, 'LaunchApp2'], [0xe022, 'MediaPlayPause'], [0xe024, 'MediaStop'], [0xe02c, 'Eject'], [0xe02e, 'AudioVolumeDown'], [0xe030, 'AudioVolumeUp'], [0xe032, 'BrowserHome'], [0xe035, 'NumpadDivide'], [0xe037, 'PrintScreen'], [0xe038, 'AltRight'], [0xe03b, 'Help'], [0xe045, 'NumLock'], [0xe046, 'Pause'], [0xe047, 'Home'], [0xe048, 'ArrowUp'], [0xe049, 'PageUp'], [0xe04b, 'ArrowLeft'], [0xe04d, 'ArrowRight'], [0xe04f, 'End'], [0xe050, 'ArrowDown'], [0xe051, 'PageDown'], [0xe052, 'Insert'], [0xe053, 'Delete'], [0xe05b, 'MetaLeft'], [0xe05c, 'MetaRight'], [0xe05d, 'ContextMenu'], [0xe05e, 'Power'], [0xe05f, 'Sleep'], [0xe063, 'WakeUp'], [0xe065, 'BrowserSearch'], [0xe066, 'BrowserFavorites'], [0xe067, 'BrowserRefresh'], [0xe068, 'BrowserStop'], [0xe069, 'BrowserForward'], [0xe06a, 'BrowserBack'], [0xe06b, 'LaunchApp1'], [0xe06c, 'LaunchMail'], [0xe06d, 'MediaSelect'], ]); const WINDOWS_VIRTUAL_KEY_CODES: ReadonlyMap = new Map([ [0x08, 'Backspace'], [0x09, 'Tab'], [0x0c, 'Numpad5'], [0x0d, 'Enter'], [0x10, 'ShiftLeft'], [0x11, 'ControlLeft'], [0x12, 'AltLeft'], [0x13, 'Pause'], [0x14, 'CapsLock'], [0x15, 'KanaMode'], [0x1b, 'Escape'], [0x1c, 'Convert'], [0x1d, 'NonConvert'], [0x20, 'Space'], [0x21, 'PageUp'], [0x22, 'PageDown'], [0x23, 'End'], [0x24, 'Home'], [0x25, 'ArrowLeft'], [0x26, 'ArrowUp'], [0x27, 'ArrowRight'], [0x28, 'ArrowDown'], [0x2c, 'PrintScreen'], [0x2d, 'Insert'], [0x2e, 'Delete'], ...Array.from({length: 10}, (_, index): [number, string] => [0x30 + index, `Digit${index}`]), ...Array.from({length: 26}, (_, index): [number, string] => [ 0x41 + index, `Key${String.fromCharCode(0x41 + index)}`, ]), [0x5b, 'MetaLeft'], [0x5c, 'MetaRight'], [0x5d, 'ContextMenu'], [0x5f, 'Sleep'], ...Array.from({length: 10}, (_, index): [number, string] => [0x60 + index, `Numpad${index}`]), [0x6a, 'NumpadMultiply'], [0x6b, 'NumpadAdd'], [0x6c, 'NumpadComma'], [0x6d, 'NumpadSubtract'], [0x6e, 'NumpadDecimal'], [0x6f, 'NumpadDivide'], ...Array.from({length: 24}, (_, index): [number, string] => [0x70 + index, `F${index + 1}`]), [0x90, 'NumLock'], [0x91, 'ScrollLock'], [0x92, 'NumpadEqual'], [0xa0, 'ShiftLeft'], [0xa1, 'ShiftRight'], [0xa2, 'ControlLeft'], [0xa3, 'ControlRight'], [0xa4, 'AltLeft'], [0xa5, 'AltRight'], [0xa6, 'BrowserBack'], [0xa7, 'BrowserForward'], [0xa8, 'BrowserRefresh'], [0xa9, 'BrowserStop'], [0xaa, 'BrowserSearch'], [0xab, 'BrowserFavorites'], [0xac, 'BrowserHome'], [0xad, 'AudioVolumeMute'], [0xae, 'AudioVolumeDown'], [0xaf, 'AudioVolumeUp'], [0xb0, 'MediaTrackNext'], [0xb1, 'MediaTrackPrevious'], [0xb2, 'MediaStop'], [0xb3, 'MediaPlayPause'], [0xb4, 'LaunchMail'], [0xb5, 'MediaSelect'], [0xb6, 'LaunchApp1'], [0xb7, 'LaunchApp2'], [0xba, 'Semicolon'], [0xbb, 'Equal'], [0xbc, 'Comma'], [0xbd, 'Minus'], [0xbe, 'Period'], [0xbf, 'Slash'], [0xc0, 'Backquote'], [0xdb, 'BracketLeft'], [0xdc, 'Backslash'], [0xdd, 'BracketRight'], [0xde, 'Quote'], [0xe2, 'IntlBackslash'], ]); const MACOS_KEY_CODES: ReadonlyMap = new Map([ [0x00, 'KeyA'], [0x01, 'KeyS'], [0x02, 'KeyD'], [0x03, 'KeyF'], [0x04, 'KeyH'], [0x05, 'KeyG'], [0x06, 'KeyZ'], [0x07, 'KeyX'], [0x08, 'KeyC'], [0x09, 'KeyV'], [0x0a, 'IntlBackslash'], [0x0b, 'KeyB'], [0x0c, 'KeyQ'], [0x0d, 'KeyW'], [0x0e, 'KeyE'], [0x0f, 'KeyR'], [0x10, 'KeyY'], [0x11, 'KeyT'], [0x12, 'Digit1'], [0x13, 'Digit2'], [0x14, 'Digit3'], [0x15, 'Digit4'], [0x16, 'Digit6'], [0x17, 'Digit5'], [0x18, 'Equal'], [0x19, 'Digit9'], [0x1a, 'Digit7'], [0x1b, 'Minus'], [0x1c, 'Digit8'], [0x1d, 'Digit0'], [0x1e, 'BracketRight'], [0x1f, 'KeyO'], [0x20, 'KeyU'], [0x21, 'BracketLeft'], [0x22, 'KeyI'], [0x23, 'KeyP'], [0x24, 'Enter'], [0x25, 'KeyL'], [0x26, 'KeyJ'], [0x27, 'Quote'], [0x28, 'KeyK'], [0x29, 'Semicolon'], [0x2a, 'Backslash'], [0x2b, 'Comma'], [0x2c, 'Slash'], [0x2d, 'KeyN'], [0x2e, 'KeyM'], [0x2f, 'Period'], [0x30, 'Tab'], [0x31, 'Space'], [0x32, 'Backquote'], [0x33, 'Backspace'], [0x35, 'Escape'], [0x36, 'MetaRight'], [0x37, 'MetaLeft'], [0x38, 'ShiftLeft'], [0x39, 'CapsLock'], [0x3a, 'AltLeft'], [0x3b, 'ControlLeft'], [0x3c, 'ShiftRight'], [0x3d, 'AltRight'], [0x3e, 'ControlRight'], [0x40, 'F17'], [0x41, 'NumpadDecimal'], [0x43, 'NumpadMultiply'], [0x45, 'NumpadAdd'], [0x47, 'NumLock'], [0x48, 'AudioVolumeUp'], [0x49, 'AudioVolumeDown'], [0x4a, 'AudioVolumeMute'], [0x4b, 'NumpadDivide'], [0x4c, 'NumpadEnter'], [0x4e, 'NumpadSubtract'], [0x4f, 'F18'], [0x50, 'F19'], [0x51, 'NumpadEqual'], [0x52, 'Numpad0'], [0x53, 'Numpad1'], [0x54, 'Numpad2'], [0x55, 'Numpad3'], [0x56, 'Numpad4'], [0x57, 'Numpad5'], [0x58, 'Numpad6'], [0x59, 'Numpad7'], [0x5a, 'F20'], [0x5b, 'Numpad8'], [0x5c, 'Numpad9'], [0x5d, 'IntlYen'], [0x5e, 'IntlRo'], [0x5f, 'NumpadComma'], [0x60, 'F5'], [0x61, 'F6'], [0x62, 'F7'], [0x63, 'F3'], [0x64, 'F8'], [0x65, 'F9'], [0x66, 'Lang2'], [0x67, 'F11'], [0x68, 'Lang1'], [0x69, 'F13'], [0x6a, 'F16'], [0x6b, 'F14'], [0x6d, 'F10'], [0x6e, 'ContextMenu'], [0x6f, 'F12'], [0x71, 'F15'], [0x72, 'Insert'], [0x73, 'Home'], [0x74, 'PageUp'], [0x75, 'Delete'], [0x76, 'F4'], [0x77, 'End'], [0x78, 'F2'], [0x79, 'PageDown'], [0x7a, 'F1'], [0x7b, 'ArrowLeft'], [0x7c, 'ArrowRight'], [0x7d, 'ArrowDown'], [0x7e, 'ArrowUp'], ]); const X11_KEYCODE_OFFSET = 8; const WINDOWS_EXTENDED_SCAN_PREFIX = 0xe000; const NAMED_KEY_CODES: Readonly> = { Spacebar: 'Space', Esc: 'Escape', Return: 'Enter', Break: 'Pause', Clear: 'NumLock', Apps: 'ContextMenu', Up: 'ArrowUp', Down: 'ArrowDown', Left: 'ArrowLeft', Right: 'ArrowRight', Del: 'Delete', LaunchMediaPlayer: 'MediaSelect', }; const KNOWN_DOM_CODES: ReadonlySet = new Set([ ...EVDEV_KEY_CODES.values(), ...WINDOWS_SCAN_CODES.values(), ...MACOS_KEY_CODES.values(), ]); export const MODIFIER_KEY_CODES = { ctrl: ['ControlLeft', 'ControlRight'], alt: ['AltLeft', 'AltRight'], shift: ['ShiftLeft', 'ShiftRight'], meta: ['MetaLeft', 'MetaRight'], } as const; export type ModifierKind = keyof typeof MODIFIER_KEY_CODES; export function modifierKindForCode(code: string | null): ModifierKind | null { if (code === null) return null; for (const kind of Object.keys(MODIFIER_KEY_CODES) as Array) { if ((MODIFIER_KEY_CODES[kind] as ReadonlyArray).includes(code)) return kind; } return null; } export function evdevKeyToDomCode(keycode: number): string | null { return EVDEV_KEY_CODES.get(keycode) ?? null; } export function x11KeycodeToDomCode(keycode: number): string | null { if (keycode < X11_KEYCODE_OFFSET) return null; return evdevKeyToDomCode(keycode - X11_KEYCODE_OFFSET); } export function windowsScanCodeKey(scanCode: number, extended: boolean): number { return extended ? WINDOWS_EXTENDED_SCAN_PREFIX | scanCode : scanCode; } export function windowsKeyToDomCode(scanCode: number, extended: boolean, virtualKey: number): string | null { const fromScanCode = scanCode === 0 ? undefined : WINDOWS_SCAN_CODES.get(windowsScanCodeKey(scanCode, extended)); return fromScanCode ?? WINDOWS_VIRTUAL_KEY_CODES.get(virtualKey) ?? null; } export function macosKeycodeToDomCode(keycode: number): string | null { return MACOS_KEY_CODES.get(keycode) ?? null; } export function hookKeyNameToDomCode(name: string): string | null { if (/^[A-Z]$/.test(name)) return `Key${name}`; if (/^[0-9]$/.test(name)) return `Digit${name}`; const named = NAMED_KEY_CODES[name]; if (named) return named; return KNOWN_DOM_CODES.has(name) ? name : null; } export function usLayoutKeyToDomCode(key: string): string | null { if (key.length === 1) { const punctuation = PUNCTUATION_KEY_CODES[key]; if (punctuation) return punctuation; const upper = key.toUpperCase(); if (/^[A-Z]$/.test(upper)) return `Key${upper}`; if (/^[0-9]$/.test(upper)) return `Digit${upper}`; return null; } return hookKeyNameToDomCode(key); } const XDG_CODE_KEYSYMS: ReadonlyMap = new Map([ ...Array.from({length: 26}, (_, index): [string, string] => [ `Key${String.fromCharCode(0x41 + index)}`, String.fromCharCode(0x61 + index), ]), ...Array.from({length: 10}, (_, index): [string, string] => [`Digit${index}`, `${index}`]), ...Array.from({length: 24}, (_, index): [string, string] => [`F${index + 1}`, `F${index + 1}`]), ['Space', 'space'], ['Enter', 'Return'], ['Tab', 'Tab'], ['Escape', 'Escape'], ['Backspace', 'BackSpace'], ['Minus', 'minus'], ['Equal', 'equal'], ['BracketLeft', 'bracketleft'], ['BracketRight', 'bracketright'], ['Backslash', 'backslash'], ['Semicolon', 'semicolon'], ['Quote', 'apostrophe'], ['Backquote', 'grave'], ['Comma', 'comma'], ['Period', 'period'], ['Slash', 'slash'], ['Home', 'Home'], ['End', 'End'], ['PageUp', 'Prior'], ['PageDown', 'Next'], ['Insert', 'Insert'], ['Delete', 'Delete'], ['ArrowLeft', 'Left'], ['ArrowRight', 'Right'], ['ArrowUp', 'Up'], ['ArrowDown', 'Down'], ['PrintScreen', 'Print'], ['ScrollLock', 'Scroll_Lock'], ['Pause', 'Pause'], ['ContextMenu', 'Menu'], ['MediaPlayPause', 'XF86AudioPlay'], ['MediaTrackNext', 'XF86AudioNext'], ['MediaTrackPrevious', 'XF86AudioPrev'], ['MediaStop', 'XF86AudioStop'], ['AudioVolumeMute', 'XF86AudioMute'], ]); const XDG_KEYSYM_PATTERN = /^[A-Za-z0-9_]+$/; function xdgKeysymForCombo(combo: {key: string; code?: string}): string | null { if (/^[a-z]$/i.test(combo.key) && !combo.code?.startsWith('Numpad')) return combo.key.toLowerCase(); const code = combo.code ?? usLayoutKeyToDomCode(combo.key); if (code === null) return null; const keysym = XDG_CODE_KEYSYMS.get(code) ?? null; return keysym !== null && XDG_KEYSYM_PATTERN.test(keysym) ? keysym : null; } export interface XdgTriggerCombo { key: string; code?: string; ctrl: boolean; alt: boolean; shift: boolean; meta: boolean; mouseButton?: number; modifierOnly?: boolean; bothSides?: boolean; } export function comboToXdgTrigger(combo: XdgTriggerCombo): string | null { if (combo.mouseButton !== undefined || combo.modifierOnly || combo.bothSides) return null; const keysym = xdgKeysymForCombo(combo); if (keysym === null) return null; const parts: Array = []; if (combo.ctrl) parts.push('CTRL'); if (combo.alt) parts.push('ALT'); if (combo.shift) parts.push('SHIFT'); if (combo.meta) parts.push('LOGO'); parts.push(keysym); return parts.join('+'); }