From aa55140219032449be29482b5b3727038987d77b Mon Sep 17 00:00:00 2001 From: Jan Wille Date: Sat, 17 Sep 2022 14:45:00 +0200 Subject: [PATCH 1/7] Mockup for ContextManager --- readchar/_posix_read.py | 32 ++++++++++++++++++++++++++++++++ readchar/_win_read.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/readchar/_posix_read.py b/readchar/_posix_read.py index 37f456f..bb1d8d8 100644 --- a/readchar/_posix_read.py +++ b/readchar/_posix_read.py @@ -4,6 +4,38 @@ from ._config import config +class ReadChar: + """A ContextManager allowing for keypress collection without requiering the user to + confirm presses with ENTER. Can be used non-blocking while inside the context.""" + + def __init__(self, cfg: config = None) -> None: + self.config = cfg if cfg is not None else config + + def __enter__(self) -> "ReadChar": + raise NotImplementedError("ToDo") + return self + + def __exit__(self, type, value, traceback) -> None: + raise NotImplementedError("ToDo") + + @property + def key_waiting(self) -> bool: + """True if a key has been pressed and is waiting to be read. False if not.""" + raise NotImplementedError("ToDo") + + def char(self) -> str: + """Reads a singel char from the input stream and returns it as a string of + length one. Does not require the user to press ENTER.""" + raise NotImplementedError("ToDo") + + def key(self) -> str: + """Reads a keypress from the input stream and returns it as a string. Keypressed + consisting of multiple characterrs will be read completly and be returned as a + string matching the definitions in `key.py`. + Does not require the user to press ENTER.""" + raise NotImplementedError("ToDo") + + # Initially taken from: # http://code.activestate.com/recipes/134892/ # Thanks to Danny Yoo diff --git a/readchar/_win_read.py b/readchar/_win_read.py index c3c51c7..7cb21b0 100644 --- a/readchar/_win_read.py +++ b/readchar/_win_read.py @@ -3,6 +3,38 @@ from ._config import config +class ReadChar: + """A ContextManager allowing for keypress collection without requiering the user to + confirm presses with ENTER. Can be used non-blocking while inside the context.""" + + def __init__(self, cfg: config = None) -> None: + self.config = cfg if cfg is not None else config + + def __enter__(self) -> "ReadChar": + raise NotImplementedError("ToDo") + return self + + def __exit__(self, type, value, traceback) -> None: + raise NotImplementedError("ToDo") + + @property + def key_waiting(self) -> bool: + """True if a key has been pressed and is waiting to be read. False if not.""" + raise NotImplementedError("ToDo") + + def char(self) -> str: + """Reads a singel char from the input stream and returns it as a string of + length one. Does not require the user to press ENTER.""" + raise NotImplementedError("ToDo") + + def key(self) -> str: + """Reads a keypress from the input stream and returns it as a string. Keypressed + consisting of multiple characterrs will be read completly and be returned as a + string matching the definitions in `key.py`. + Does not require the user to press ENTER.""" + raise NotImplementedError("ToDo") + + def readchar() -> str: """Reads a single character from the input stream. Blocks until a character is available.""" From 59d739f4e718330da3173f968fd28e24b87bc8be Mon Sep 17 00:00:00 2001 From: Jan Wille Date: Sat, 17 Sep 2022 15:10:08 +0200 Subject: [PATCH 2/7] implement windows ContextManager --- readchar/_win_read.py | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/readchar/_win_read.py b/readchar/_win_read.py index 7cb21b0..bfc8049 100644 --- a/readchar/_win_read.py +++ b/readchar/_win_read.py @@ -1,5 +1,7 @@ import msvcrt +import signal +from . import _win_key as key from ._config import config @@ -7,32 +9,50 @@ class ReadChar: """A ContextManager allowing for keypress collection without requiering the user to confirm presses with ENTER. Can be used non-blocking while inside the context.""" + @staticmethod + def __silent_CTRL_C_callback(signum, frame): + msvcrt.ungetch(key.CTRL_C.encode("ascii")) + def __init__(self, cfg: config = None) -> None: self.config = cfg if cfg is not None else config def __enter__(self) -> "ReadChar": - raise NotImplementedError("ToDo") + self.__org_SIGBREAK_handler = signal.getsignal(signal.SIGBREAK) + self.__org_SIGINT_handler = signal.getsignal(signal.SIGINT) + signal.signal(signal.SIGBREAK, signal.default_int_handler) + signal.signal(signal.SIGINT, ReadChar.__silent_CTRL_C_callback) return self def __exit__(self, type, value, traceback) -> None: - raise NotImplementedError("ToDo") + signal.signal(signal.SIGBREAK, self.__org_SIGBREAK_handler) + signal.signal(signal.SIGINT, self.__org_SIGINT_handler) @property def key_waiting(self) -> bool: """True if a key has been pressed and is waiting to be read. False if not.""" - raise NotImplementedError("ToDo") + return msvcrt.kbhit() def char(self) -> str: """Reads a singel char from the input stream and returns it as a string of length one. Does not require the user to press ENTER.""" - raise NotImplementedError("ToDo") + return msvcrt.getch().decode("latin1") def key(self) -> str: """Reads a keypress from the input stream and returns it as a string. Keypressed consisting of multiple characterrs will be read completly and be returned as a string matching the definitions in `key.py`. Does not require the user to press ENTER.""" - raise NotImplementedError("ToDo") + c = self.char() + + if c in self.config.INTERRUPT_KEYS: + raise KeyboardInterrupt + + # if it is a normal character: + if c not in "\x00\xe0": + return c + + # if it is a special key, read second half: + return "\x00" + self.char() def readchar() -> str: From 2b67672441b15fe60b15b78d5b9eab26d096fa0b Mon Sep 17 00:00:00 2001 From: Jan Wille Date: Sat, 17 Sep 2022 15:22:18 +0200 Subject: [PATCH 3/7] implement posix ContextManager --- readchar/_posix_read.py | 37 ++++++++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/readchar/_posix_read.py b/readchar/_posix_read.py index bb1d8d8..59b36ac 100644 --- a/readchar/_posix_read.py +++ b/readchar/_posix_read.py @@ -1,5 +1,7 @@ import sys import termios +from copy import copy +from select import select from ._config import config @@ -12,28 +14,53 @@ def __init__(self, cfg: config = None) -> None: self.config = cfg if cfg is not None else config def __enter__(self) -> "ReadChar": - raise NotImplementedError("ToDo") + self.fd = sys.stdin.fileno() + term = termios.tcgetattr(self.fd) + self.old_settings = copy(term) + term[3] &= ~(termios.ICANON | termios.ECHO | termios.IGNBRK | termios.BRKINT) + termios.tcsetattr(self.fd, termios.TCSAFLUSH, term) return self def __exit__(self, type, value, traceback) -> None: - raise NotImplementedError("ToDo") + termios.tcsetattr(self.fd, termios.TCSADRAIN, self.old_settings) @property def key_waiting(self) -> bool: """True if a key has been pressed and is waiting to be read. False if not.""" - raise NotImplementedError("ToDo") + return sys.stdin in select([sys.stdin], [], [], 0)[0] def char(self) -> str: """Reads a singel char from the input stream and returns it as a string of length one. Does not require the user to press ENTER.""" - raise NotImplementedError("ToDo") + return sys.stdin.read(1) def key(self) -> str: """Reads a keypress from the input stream and returns it as a string. Keypressed consisting of multiple characterrs will be read completly and be returned as a string matching the definitions in `key.py`. Does not require the user to press ENTER.""" - raise NotImplementedError("ToDo") + c1 = self.char() + + if c1 in self.config.INTERRUPT_KEYS: + raise KeyboardInterrupt + + if c1 != "\x1B": + return c1 + + c2 = self.char() + if c2 not in "\x4F\x5B": + return c1 + c2 + + c3 = self.char() + if c3 not in "\x31\x32\x33\x35\x36": + return c1 + c2 + c3 + + c4 = self.char() + if c4 not in "\x30\x31\x33\x34\x35\x37\x38\x39": + return c1 + c2 + c3 + c4 + + c5 = self.char() + return c1 + c2 + c3 + c4 + c5 # Initially taken from: From 73230f17e4f6a7ca818c4157254c5ebfde311fa8 Mon Sep 17 00:00:00 2001 From: Jan Wille Date: Fri, 21 Oct 2022 20:52:41 +0200 Subject: [PATCH 4/7] increment dev-version --- readchar/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readchar/__init__.py b/readchar/__init__.py index 9f1bac2..fbbef73 100644 --- a/readchar/__init__.py +++ b/readchar/__init__.py @@ -1,6 +1,6 @@ """Library to easily read single chars and key strokes""" -__version__ = "4.0.4-dev0" +__version__ = "4.0.4-dev1" __all__ = ["readchar", "readkey", "key", "config"] from sys import platform From a2bcc11a4ad63224cd49db667ee68a58ba139426 Mon Sep 17 00:00:00 2001 From: Jan Wille Date: Sun, 6 Nov 2022 18:20:35 +0100 Subject: [PATCH 5/7] posix: wrap stdin in a cutom buffer select() is not reliable to as an alternative to kbhit(), it only works one time an will than returne false untill the next key-press by the user. Now the content of stdin is read completly and put in a custom buffer, which allows for peeking of data. --- readchar/__init__.py | 2 +- readchar/_posix_read.py | 39 +++++++++++++++++++++++++++++---------- 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/readchar/__init__.py b/readchar/__init__.py index fbbef73..7675304 100644 --- a/readchar/__init__.py +++ b/readchar/__init__.py @@ -1,6 +1,6 @@ """Library to easily read single chars and key strokes""" -__version__ = "4.0.4-dev1" +__version__ = "4.0.4-dev2" __all__ = ["readchar", "readkey", "key", "config"] from sys import platform diff --git a/readchar/_posix_read.py b/readchar/_posix_read.py index 59b36ac..463538f 100644 --- a/readchar/_posix_read.py +++ b/readchar/_posix_read.py @@ -1,6 +1,7 @@ import sys import termios from copy import copy +from io import StringIO from select import select from ._config import config @@ -12,53 +13,71 @@ class ReadChar: def __init__(self, cfg: config = None) -> None: self.config = cfg if cfg is not None else config + self._buffer = StringIO() def __enter__(self) -> "ReadChar": self.fd = sys.stdin.fileno() term = termios.tcgetattr(self.fd) self.old_settings = copy(term) - term[3] &= ~(termios.ICANON | termios.ECHO | termios.IGNBRK | termios.BRKINT) + + term[3] &= ~( + termios.ICANON # don't require ENTER + | termios.ECHO # don't echo + | termios.IGNBRK + | termios.BRKINT + ) + term[6][termios.VMIN] = 0 # imideatly process every input + term[6][termios.VTIME] = 0 termios.tcsetattr(self.fd, termios.TCSAFLUSH, term) return self def __exit__(self, type, value, traceback) -> None: - termios.tcsetattr(self.fd, termios.TCSADRAIN, self.old_settings) + termios.tcsetattr(self.fd, termios.TCSAFLUSH, self.old_settings) + + def __update(self) -> None: + """check stdin and update the interal buffer if it holds data""" + if sys.stdin in select([sys.stdin], [], [], 0)[0]: + pos = self._buffer.tell() + data = sys.stdin.read() + self._buffer.write(data) + self._buffer.seek(pos) @property def key_waiting(self) -> bool: """True if a key has been pressed and is waiting to be read. False if not.""" - return sys.stdin in select([sys.stdin], [], [], 0)[0] + self.__update() + pos = self._buffer.tell() + next_byte = self._buffer.read(1) + self._buffer.seek(pos) + return bool(next_byte) def char(self) -> str: """Reads a singel char from the input stream and returns it as a string of length one. Does not require the user to press ENTER.""" - return sys.stdin.read(1) + self.__update() + return self._buffer.read(1) def key(self) -> str: """Reads a keypress from the input stream and returns it as a string. Keypressed consisting of multiple characterrs will be read completly and be returned as a string matching the definitions in `key.py`. Does not require the user to press ENTER.""" - c1 = self.char() + self.__update() + c1 = self.char() if c1 in self.config.INTERRUPT_KEYS: raise KeyboardInterrupt - if c1 != "\x1B": return c1 - c2 = self.char() if c2 not in "\x4F\x5B": return c1 + c2 - c3 = self.char() if c3 not in "\x31\x32\x33\x35\x36": return c1 + c2 + c3 - c4 = self.char() if c4 not in "\x30\x31\x33\x34\x35\x37\x38\x39": return c1 + c2 + c3 + c4 - c5 = self.char() return c1 + c2 + c3 + c4 + c5 From d9d86dba53c345264574bda1d0b286e2742f6bfa Mon Sep 17 00:00:00 2001 From: Jan Wille Date: Sun, 6 Nov 2022 18:20:42 +0100 Subject: [PATCH 6/7] make the new class public --- readchar/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/readchar/__init__.py b/readchar/__init__.py index 7675304..22db150 100644 --- a/readchar/__init__.py +++ b/readchar/__init__.py @@ -1,7 +1,7 @@ """Library to easily read single chars and key strokes""" __version__ = "4.0.4-dev2" -__all__ = ["readchar", "readkey", "key", "config"] +__all__ = ["readchar", "readkey", "ReadChar", "key", "config"] from sys import platform @@ -10,9 +10,9 @@ if platform.startswith(("linux", "darwin", "freebsd")): from . import _posix_key as key - from ._posix_read import readchar, readkey + from ._posix_read import ReadChar, readchar, readkey elif platform in ("win32", "cygwin"): from . import _win_key as key - from ._win_read import readchar, readkey + from ._win_read import ReadChar, readchar, readkey else: raise NotImplementedError(f"The platform {platform} is not supported yet") From cd10b14841886a2abb79792d8bbba5761649ee40 Mon Sep 17 00:00:00 2001 From: Cube707 Date: Wed, 16 Nov 2022 09:52:26 +0000 Subject: [PATCH 7/7] increment version after release --- readchar/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readchar/__init__.py b/readchar/__init__.py index 22db150..5687bb8 100644 --- a/readchar/__init__.py +++ b/readchar/__init__.py @@ -1,6 +1,6 @@ """Library to easily read single chars and key strokes""" -__version__ = "4.0.4-dev2" +__version__ = "4.1.0-dev2" __all__ = ["readchar", "readkey", "ReadChar", "key", "config"] from sys import platform