All too often I find myself wanting to allow only a certain list of characters to be written to
stdin, and only recently did I actually bother to implement it. In Python, of all languages!
Essentially, this module provides a few APIs that allow a very tailored approach to reading characters from a terminal. By intercepting individual keypresses at the instant they occur, we can make cool decisions about closing the input stream whenever we want -- we dictate who says what in our terminal!
The standard input stream, after being opened, can be closed after a number of characters, or, at caller's option, any combination of a number of characters and allowed inputs.
read_single_keypress() -> string— cross-platform function that gets exactly one keypress, and returns it after processing.
thismany(count: int = -1) -> string— get exactly
countcharacters from stdin. if count is
sys.maxsizechars will be read.
until(chars: string || list, count: int = -1) -> string— get characters from stdin until
charis read, or until
countchars have been read. if
sys.maxsizechars will be read.
until_not(chars: string || list, count: int = -1) -> string— get characters from stdin until any of
charsis not read. if
sys.maxsizechars will be read.
pretty_press() -> string— read a char from stdin, and send it through the same processing as the other functions here do — write it, then if it's a backspace write a backspace, etc
_Getch: determines system platform and calls one of
_GetchUnix: get a raw character from stdin, on any *nx box
_GetchWindows: get a raw character from stdin, on any Windows box
nbsp: do stuff accordingly for certain chars of input; handles backspace, etc
parsenum: return a number, or
sys.maxsizeif number is
import sys class _Getch: """Gets a single character from standard input.""" def __init__(self): try: self.impl = _GetchWindows() except ImportError: self.impl = _GetchUnix() def __call__(self): return self.impl() class _GetchUnix: def __init__(self): import tty def __call__(self): import tty, termios fd = sys.stdin.fileno() old_settings = termios.tcgetattr(fd) try: tty.setraw(sys.stdin.fileno()) ch = sys.stdin.read(1) finally: termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) return ch class _GetchWindows: def __init__(self): import msvcrt def __call__(self): import msvcrt return msvcrt.getch() parsenum = (lambda num: (sys.maxsize if 0 > num else num)) def read_single_keypress(): """interface for _Getch that interprets backspace and DEL properly""" getch = _Getch() x = getch.__call__() ox = ord(x) if ox == 27 or ox == 127: sys.stdout.write(chr(8)) sys.stdout.write(chr(32)) # hacky? indeed. does it *work*? hell yeah! sys.stdout.write(chr(8)) elif ox == 3: raise KeyboardInterrupt elif ox == 4: raise EOFError return x def nbsp(x, y): """append x to y as long as x is not DEL or backspace""" if ord(x) == 27 or ord(x) == 127: try: y.pop() except IndexError: pass return y y.append(x) return y def thismany(count=-1) -> str: """get exactly count chars of stdin""" y =  count = parsenum(count) while len(y) <= count: i = read_single_keypress() _ = sys.stdout.write(i) sys.stdout.flush() y = nbsp(i, y) return "".join(y) def until(chars, count=-1) -> str: """get chars of stdin until any of chars is read, or until count chars have been read, whichever comes first""" y =  chars = list(chars) count = parsenum(count) while len(y) <= count: i = read_single_keypress() _ = sys.stdout.write(i) sys.stdout.flush() if i in chars: break y = nbsp(i, y) return "".join(y) def until_not(chars, count=-1) -> str: """read stdin until any of chars stop being read, or until count chars have been read; whichever comes first""" y =  chars = list(chars) count = parsenum(count) while len(y) <= count: i = read_single_keypress() _ = sys.stdout.write(i) sys.stdout.flush() if i not in chars: break y = nbsp(i, y) return "".join(y) def pretty_press() -> str: """literally just read any fancy char from stdin let caller do whatever""" i = read_single_keypress() _ = sys.stdout.write(i) sys.stdout.flush() return nbsp(i, y)
Short & sweet. No bugs. Anywhere. 1
Notes / etc
There's a lot of duplication here, and I hate it. I turned the whole of the outward functions into one big function earlier today, with lots of optional args and a lot of nested
if's and it was bad. I want to avoid duplication as much as possible, because there's a one keyword difference between some of these functions and I can't see a clean way to fix this.
Yeah, I know all of the "inward interface" functions should begin with an underscore. Who knows, maybe caller will want them if caller implements
Backspaces work! That was a lot of headache to finally get compliant. Arrow keys, on the other hand, do not work. In fact, they are so badly broken that how well they work has wrapped the scale. Try it!
To extend the last point, backspaces work too well in that you can put the cursor anywhere you want on the terminal grid, and backspace whatever you like. Buggy? Heck yeah! It's probably fine, because I'm not building
I have not tested this much on Py2, but it should work fine. If it blows up your cat, I am not responsible for damages because this software is released under the GPL.
Naming. My names for things are terrible. They say the two hardest things in programming are cache invalidation and naming things. I don't have to worry about cache invalidation in Python, so I screwed up naming instead.
You'll probably want some way to... y'know, use this API without having to write code. Understandable, so here you go: some example functions. Look how simple my API is!
def _until_demo() -> None: """demonstrate the until function""" print("get until what?") char = read_single_keypress() _ = sys.stdout.write(char + "\n") sys.stdout.flush() y = until(char) print("\n" + y) def _thismany_demo() -> None: """demonstrate the thismany function""" print("get how many chars?") kps = input() try: kps = int(kps) except ValueError: print("not a number, sorry") return print("getting", str(kps)) y = thismany(kps) print("\n" + y) def _can_you_vote() -> str: """a practical example: test if a user can vote based purely on keypresses""" _ = sys.stdout.write("can you vote? age : ") sys.stdout.flush() x = int("0" + until_not("0123456789")) if not x: print("\nsorry, age can only consist of digits.") return print( "your age is", x, "\nYou can vote!" if x >= 18 else "Sorry! you can't vote" ) def _forth_syntax_test() -> str: """ in the programming language Forth, `function` definitons start at the beginning of a line with a `:` colon and go until the next semicolon. this is an example of how this module can be used in a Forth REPL to compile statements specially; it's implemented in catb0t/microcat as well. """ sys.stdout.write("demo FORTH repl \n> ") firstchar = read_single_keypress() _ = sys.stdout.write(firstchar) if firstchar != ":": return print("first char wasn't ':'") defn = firstchar + until(";") + ";" sys.stdout.write("\nrepl got:\n" + defn + "\n")
If a review should focus on any one thing, it should be duplication, because it's really bad and it makes me sad :(
1unit testing hand crafted python stdin in python is hard :(