diff --git a/bin/kitty_keytester.py b/bin/kitty_keytester.py index 8976c133b..073c34d1c 100644 --- a/bin/kitty_keytester.py +++ b/bin/kitty_keytester.py @@ -1,9 +1,11 @@ -from termios import tcgetattr, tcsetattr, TCSADRAIN, TIOCGWINSZ +from termios import tcgetattr, tcsetattr, TCSADRAIN, TIOCGWINSZ, TCSAFLUSH from tty import setcbreak, setraw import os +import sys from contextlib import contextmanager from select import select from time import sleep +from json import dumps, loads from typing import Optional, Tuple, Union @@ -23,7 +25,7 @@ class TerminalContext: return TerminalContext(fd, True) def close(self) -> None: - tcsetattr(self.fd, TCSADRAIN, self._initial_attr) + tcsetattr(self.fd, TCSAFLUSH, self._initial_attr) if self.close_fd: os.close(self.fd) @@ -45,11 +47,11 @@ class TerminalContext: return tattr = self.termios_attributes try: - setcbreak(self.fd, TCSADRAIN) + setcbreak(self.fd, TCSAFLUSH) self.is_cbreak = True yield finally: - tcsetattr(self.fd, TCSADRAIN, tattr) + tcsetattr(self.fd, TCSAFLUSH, tattr) self.is_cbreak = False @contextmanager @@ -62,11 +64,11 @@ class TerminalContext: return tattr = self.termios_attributes try: - setraw(self.fd, TCSADRAIN) + setraw(self.fd, TCSAFLUSH) self.is_raw = True yield finally: - tcsetattr(self.fd, TCSADRAIN, tattr) + tcsetattr(self.fd, TCSAFLUSH, tattr) self.is_raw = False @contextmanager @@ -82,11 +84,12 @@ class TerminalContext: if undo: undo() - def write(self, s: Union[str, bytes]) -> None: + def write(self, data: Union[str, bytes]) -> None: """ Write string or bytes directly to the terminal. """ - data = s if isinstance(s, bytes) else s.encode('utf-8') + if isinstance(data, str): + data = data.encode('utf-8') sent = os.write(self.fd, data) while sent: data = data[sent:] @@ -99,7 +102,9 @@ class TerminalContext: empty bytes are returned. """ can_read, _, _ = select([self.fd], [], [], timeout) - return os.read(self.fd, amount) if can_read else b'' + if can_read: + return os.read(self.fd, amount) + return b'' @contextmanager @@ -111,21 +116,81 @@ def cterminal_context(): t.close() -with cterminal_context() as term: - with term.custom_state(undo=lambda:term.write('\x1b[3u') - sleep(1) - print('PRESS (within 10s) and HOLD (for 5s)\r') - data = [] - cur = term.read(timeout=10) +def extract_events(data: list[str]): + if len(data) < 5: + raise Exception('not enough reports') + types = set(data) + if len(types) == 1: + return {'PRESS': data[0], 'REPEAT': data[0], 'RELEASE': None} + if len(types) == 2: + last = data.pop() + if last != data[0] and len(set(data)) == 1: + return {'PRESS': data[0], 'REPEAT': data[0], 'RELEASE': last} + raise Exception('weird reports, 2 types') + if len(types) == 3: + first = data.pop(0) + last = data.pop() + if first != last and first != data[0] and last != data[0]: + return {'PRESS': first, 'REPEAT': data[0], 'RELEASE': last} + raise Exception('weird reports, 3 types') + raise Exception('more than 3 types') + + +def query(term: TerminalContext, mode: int): + with term.custom_state(undo=lambda:term.write('\x1b[{mode}u') + sleep(.1) + print('PRESS (within 5s) and HOLD (for 5s)\r') + data: list[bytes] = [] + cur = term.read(timeout=5) while cur: data.append(cur) - cur = term.read(timeout=1) + cur = term.read(timeout=.5) if len(data) > 5: - print('RELEASE\r') - sleep(0.5) - term.read(timeout=0.1) - print(data, '\r') + print('RELEASE\r', end='') + try: + return extract_events([b.decode('utf-8') for b in data]) + except Exception as e: + print('Error:', e, '\r') + print(data, '\r') + term.read(timeout=.1) - sleep(1) +def save(filedata: dict[str, dict[str, str]], filename, entry, mode, events): + try: + filedata[entry][mode] = events + except KeyError: + filedata[entry] = {mode: events} + with open(filename, 'w') as f: + f.write(dumps(filedata, indent=2)) + + +def main(): + mode = 1 + filedata = {} + print(sys.argv) + if len(sys.argv) != 3: + print('ERROR: not enough arguments') + print('Usage: python kitty_keytester.py ') + return + mode = int(sys.argv[1]) + filename = sys.argv[2] + if os.path.exists(filename): + with open(filename) as f: + filedata = loads(f.read()) + with cterminal_context() as term: + while True: + with term.raw_mode(): + events = query(term, mode) + if events: + print(events) + entry = input('Entry: ') + if entry: + save(filedata, filename, entry, str(mode), events) + cont = input('Continue? (y)') + if cont not in ['y', '']: + break + + +if __name__ == '__main__': + main() diff --git a/fixtures/kitty_keyboard/german_qwertz_no_modifier.json b/fixtures/kitty_keyboard/german_qwertz_no_modifier.json new file mode 100644 index 000000000..00bb9c9fe --- /dev/null +++ b/fixtures/kitty_keyboard/german_qwertz_no_modifier.json @@ -0,0 +1,632 @@ +{ + "Escape": { + "1": { + "PRESS": "\u001b[27u", + "REPEAT": "\u001b[27u", + "RELEASE": null + } + }, + "F1": { + "1": { + "PRESS": "\u001b[P", + "REPEAT": "\u001b[P", + "RELEASE": null + } + }, + "F2": { + "1": { + "PRESS": "\u001b[Q", + "REPEAT": "\u001b[Q", + "RELEASE": null + } + }, + "F3": { + "1": { + "PRESS": "\u001b[13~", + "REPEAT": "\u001b[13~", + "RELEASE": null + } + }, + "F4": { + "1": { + "PRESS": "\u001b[S", + "REPEAT": "\u001b[S", + "RELEASE": null + } + }, + "F5": { + "1": { + "PRESS": "\u001b[15~", + "REPEAT": "\u001b[15~", + "RELEASE": null + } + }, + "F6": { + "1": { + "PRESS": "\u001b[17~", + "REPEAT": "\u001b[17~", + "RELEASE": null + } + }, + "F7": { + "1": { + "PRESS": "\u001b[18~", + "REPEAT": "\u001b[18~", + "RELEASE": null + } + }, + "F8": { + "1": { + "PRESS": "\u001b[19~", + "REPEAT": "\u001b[19~", + "RELEASE": null + } + }, + "F9": { + "1": { + "PRESS": "\u001b[20~", + "REPEAT": "\u001b[20~", + "RELEASE": null + } + }, + "F10": { + "1": { + "PRESS": "\u001b[21~", + "REPEAT": "\u001b[21~", + "RELEASE": null + } + }, + "F11": { + "1": { + "PRESS": "\u001b[23~", + "REPEAT": "\u001b[23~", + "RELEASE": null + } + }, + "F12": { + "1": { + "PRESS": "\u001b[24~", + "REPEAT": "\u001b[24~", + "RELEASE": null + } + }, + "Insert": { + "1": { + "PRESS": "\u001b[2~", + "REPEAT": "\u001b[2~", + "RELEASE": null + } + }, + "Delete": { + "1": { + "PRESS": "\u001b[3~", + "REPEAT": "\u001b[3~", + "RELEASE": null + } + }, + "PageUp": { + "1": { + "PRESS": "\u001b[5~", + "REPEAT": "\u001b[5~", + "RELEASE": null + } + }, + "PageDown": { + "1": { + "PRESS": "\u001b[6~", + "REPEAT": "\u001b[6~", + "RELEASE": null + } + }, + "Home": { + "1": { + "PRESS": "\u001b[H", + "REPEAT": "\u001b[H", + "RELEASE": null + } + }, + "End": { + "1": { + "PRESS": "\u001b[F", + "REPEAT": "\u001b[F", + "RELEASE": null + } + }, + "Dead (Backquote)": { + "1": { + "PRESS": "^", + "REPEAT": "^", + "RELEASE": null + } + }, + "1 (Digit1)": { + "1": { + "PRESS": "1", + "REPEAT": "1", + "RELEASE": null + } + }, + "2 (Digit2)": { + "1": { + "PRESS": "2", + "REPEAT": "2", + "RELEASE": null + } + }, + "3 (Digit3)": { + "1": { + "PRESS": "3", + "REPEAT": "3", + "RELEASE": null + } + }, + "4 (Digit4)": { + "1": { + "PRESS": "4", + "REPEAT": "4", + "RELEASE": null + } + }, + "5 (Digit5)": { + "1": { + "PRESS": "5", + "REPEAT": "5", + "RELEASE": null + } + }, + "6 (Digit6)": { + "1": { + "PRESS": "6", + "REPEAT": "6", + "RELEASE": null + } + }, + "7 (Digit7)": { + "1": { + "PRESS": "7", + "REPEAT": "7", + "RELEASE": null + } + }, + "8 (Digit8)": { + "1": { + "PRESS": "8", + "REPEAT": "8", + "RELEASE": null + } + }, + "9 (Digit9)": { + "1": { + "PRESS": "9", + "REPEAT": "9", + "RELEASE": null + } + }, + "0 (Digit0)": { + "1": { + "PRESS": "0", + "REPEAT": "0", + "RELEASE": null + } + }, + "\u00df (Minus)": { + "1": { + "PRESS": "\u00df", + "REPEAT": "\u00df", + "RELEASE": null + } + }, + "Dead (Equal)": { + "1": { + "PRESS": "\u00b4", + "REPEAT": "\u00b4", + "RELEASE": null + } + }, + "Backspace": { + "1": { + "PRESS": "\u007f", + "REPEAT": "\u007f", + "RELEASE": null + } + }, + "Tab": { + "1": { + "PRESS": "\t", + "REPEAT": "\t", + "RELEASE": null + } + }, + "q (KeyQ)": { + "1": { + "PRESS": "q", + "REPEAT": "q", + "RELEASE": null + } + }, + "w (KeyW)": { + "1": { + "PRESS": "w", + "REPEAT": "w", + "RELEASE": null + } + }, + "e (KeyE)": { + "1": { + "PRESS": "e", + "REPEAT": "e", + "RELEASE": null + } + }, + "r (KeyR)": { + "1": { + "PRESS": "r", + "REPEAT": "r", + "RELEASE": null + } + }, + "t (KeyT)": { + "1": { + "PRESS": "t", + "REPEAT": "t", + "RELEASE": null + } + }, + "z (KeyY)": { + "1": { + "PRESS": "z", + "REPEAT": "z", + "RELEASE": null + } + }, + "u (KeyU)": { + "1": { + "PRESS": "u", + "REPEAT": "u", + "RELEASE": null + } + }, + "i (KeyI)": { + "1": { + "PRESS": "i", + "REPEAT": "i", + "RELEASE": null + } + }, + "o (KeyO)": { + "1": { + "PRESS": "o", + "REPEAT": "o", + "RELEASE": null + } + }, + "p (KeyP)": { + "1": { + "PRESS": "p", + "REPEAT": "p", + "RELEASE": null + } + }, + "\u00fc (BracketLeft)": { + "1": { + "PRESS": "\u00fc", + "REPEAT": "\u00fc", + "RELEASE": null + } + }, + "+ (BracketRight)": { + "1": { + "PRESS": "+", + "REPEAT": "+", + "RELEASE": null + } + }, + "Enter": { + "1": { + "PRESS": "\r", + "REPEAT": "\r", + "RELEASE": null + } + }, + "a (KeyA)": { + "1": { + "PRESS": "a", + "REPEAT": "a", + "RELEASE": null + } + }, + "s (KeyS)": { + "1": { + "PRESS": "s", + "REPEAT": "s", + "RELEASE": null + } + }, + "d (KeyD)": { + "1": { + "PRESS": "d", + "REPEAT": "d", + "RELEASE": null + } + }, + "f (KeyF)": { + "1": { + "PRESS": "f", + "REPEAT": "f", + "RELEASE": null + } + }, + "g (KeyG)": { + "1": { + "PRESS": "g", + "REPEAT": "g", + "RELEASE": null + } + }, + "h (KeyH)": { + "1": { + "PRESS": "h", + "REPEAT": "h", + "RELEASE": null + } + }, + "j (KeyJ)": { + "1": { + "PRESS": "j", + "REPEAT": "j", + "RELEASE": null + } + }, + "k (KeyK)": { + "1": { + "PRESS": "k", + "REPEAT": "k", + "RELEASE": null + } + }, + "l (KeyL)": { + "1": { + "PRESS": "l", + "REPEAT": "l", + "RELEASE": null + } + }, + "\u00f6 (Semicolon)": { + "1": { + "PRESS": "\u00f6", + "REPEAT": "\u00f6", + "RELEASE": null + } + }, + "\u00e4 (Quote)": { + "1": { + "PRESS": "\u00e4", + "REPEAT": "\u00e4", + "RELEASE": null + } + }, + "# (Backslash)": { + "1": { + "PRESS": "#", + "REPEAT": "#", + "RELEASE": null + } + }, + "< (IntlBackslash)": { + "1": { + "PRESS": "<", + "REPEAT": "<", + "RELEASE": null + } + }, + "y (KeyZ)": { + "1": { + "PRESS": "y", + "REPEAT": "y", + "RELEASE": null + } + }, + "x (KeyX)": { + "1": { + "PRESS": "x", + "REPEAT": "x", + "RELEASE": null + } + }, + "c (KeyC)": { + "1": { + "PRESS": "c", + "REPEAT": "c", + "RELEASE": null + } + }, + "v (KeyV)": { + "1": { + "PRESS": "v", + "REPEAT": "v", + "RELEASE": null + } + }, + "b (KeyB)": { + "1": { + "PRESS": "b", + "REPEAT": "b", + "RELEASE": null + } + }, + "n (KeyN)": { + "1": { + "PRESS": "n", + "REPEAT": "n", + "RELEASE": null + } + }, + "m (KeyM)": { + "1": { + "PRESS": "m", + "REPEAT": "m", + "RELEASE": null + } + }, + ", (Comma)": { + "1": { + "PRESS": ",", + "REPEAT": ",", + "RELEASE": null + } + }, + ". (Period)": { + "1": { + "PRESS": ".", + "REPEAT": ".", + "RELEASE": null + } + }, + "- (Slash)": { + "1": { + "PRESS": "-", + "REPEAT": "-", + "RELEASE": null + } + }, + "ArrowLeft": { + "1": { + "PRESS": "\u001b[D", + "REPEAT": "\u001b[D", + "RELEASE": null + } + }, + "ArrowUp": { + "1": { + "PRESS": "\u001b[A", + "REPEAT": "\u001b[A", + "RELEASE": null + } + }, + "ArrowRight": { + "1": { + "PRESS": "\u001b[C", + "REPEAT": "\u001b[C", + "RELEASE": null + } + }, + "ArrowDown": { + "1": { + "PRESS": "\u001b[B", + "REPEAT": "\u001b[B", + "RELEASE": null + } + }, + "/ (NumpadDivide)": { + "1": { + "PRESS": "/", + "REPEAT": "/", + "RELEASE": null + } + }, + "* (NumpadMultiply)": { + "1": { + "PRESS": "*", + "REPEAT": "*", + "RELEASE": null + } + }, + "- (NumpadSubtract)": { + "1": { + "PRESS": "-", + "REPEAT": "-", + "RELEASE": null + } + }, + "Home (Numpad7)": { + "1": { + "PRESS": "\u001b[57423u", + "REPEAT": "\u001b[57423u", + "RELEASE": null + } + }, + "ArrowUp (Numpad8)": { + "1": { + "PRESS": "\u001b[57419u", + "REPEAT": "\u001b[57419u", + "RELEASE": null + } + }, + "PageUp (Numpad9)": { + "1": { + "PRESS": "\u001b[57421u", + "REPEAT": "\u001b[57421u", + "RELEASE": null + } + }, + "+ (NumpadAdd)": { + "1": { + "PRESS": "+", + "REPEAT": "+", + "RELEASE": null + } + }, + "ArrowLeft (Numpad4)": { + "1": { + "PRESS": "\u001b[57417u", + "REPEAT": "\u001b[57417u", + "RELEASE": null + } + }, + "Clear (Numpad5)": { + "1": { + "PRESS": "\u001b[E", + "REPEAT": "\u001b[E", + "RELEASE": null + } + }, + "ArrowRight (Numpad6)": { + "1": { + "PRESS": "\u001b[57418u", + "REPEAT": "\u001b[57418u", + "RELEASE": null + } + }, + "End (Numpad1)": { + "1": { + "PRESS": "\u001b[57424u", + "REPEAT": "\u001b[57424u", + "RELEASE": null + } + }, + "ArrowDown (Numpad2)": { + "1": { + "PRESS": "\u001b[57420u", + "REPEAT": "\u001b[57420u", + "RELEASE": null + } + }, + "PageDown (Numpad3)": { + "1": { + "PRESS": "\u001b[57422u", + "REPEAT": "\u001b[57422u", + "RELEASE": null + } + }, + "Enter (NumpadEnter)": { + "1": { + "PRESS": "\u001b[57414u", + "REPEAT": "\u001b[57414u", + "RELEASE": null + } + }, + "Insert (Numpad0)": { + "1": { + "PRESS": "\u001b[57425u", + "REPEAT": "\u001b[57425u", + "RELEASE": null + } + }, + "\u0000 (NumpadDecimal)": { + "1": { + "PRESS": "\u001b[57426u", + "REPEAT": "\u001b[57426u", + "RELEASE": null + } + } +} \ No newline at end of file