Reputation: 75585
Apologies for the long code post, but I believe it is useful context.
I am playing around with parsing special keys in raw Python (without curses), but it seems that the select
trick for doing non-blocking input is not working in this scenario. In particular, it looks like after reading the first character of input, select
is returning that stdin
is not readable despite there being more characters of input to read.
ESC
followed by the remainder of the escape sequence in individual lines. Expected behavior: Output ARROW_LEFT
.Is it possible to correctly read the full escape sequence for special keys, while still reading ESC itself correctly?
#!/usr/bin/env python3
import sys
from enum import Enum
import tty
import termios
import select
import signal
# Takes a given single-character string and returns the string control version
# of it. For example, it takes 'c' and returns the string representation of
# Control-C. This can be used to check for control-x keys in the output of
# readKey.
def controlKey(c):
return chr(ord(c) & 0x1f)
def nonblock_read(stream, limit=1):
if select.select([stream,],[],[],0.1)[0]:
return stream.read(limit)
return None
# Read a key of input as a string. For special keys, it returns a
# representative string. For control keys, it returns the raw string.
# This function assumes that the caller has already put the terminal in raw mode.
def readKey():
c = nonblock_read(sys.stdin, 1)
if not c: return None
# Handle special keys represented by escape sequences
if c == "\x1b":
seq = [None] * 3
seq[0] = nonblock_read(sys.stdin, 1)
if not seq[0]: return "ESC"
seq[1] = nonblock_read(sys.stdin, 1)
if not seq[1]: return "ESC"
if seq[0] == '[':
if seq[1] >= '0' and seq[1] <= '9':
seq[2] = nonblock_read(sys.stdin, 1)
if not seq[2]: return "ESC"
if seq[2] == '~':
if seq[1] == '1': return "HOME_KEY"
if seq[1] == '3': return "DEL_KEY"
if seq[1] == '4': return "END_KEY"
if seq[1] == '5': return "PAGE_UP"
if seq[1] == '6': return "PAGE_DOWN"
if seq[1] == '7': return "HOME_KEY"
if seq[1] == '8': return "END_KEY"
else:
if seq[1] == 'A': return "ARROW_UP"
if seq[1] == 'B': return "ARROW_DOWN"
if seq[1] == 'C': return "ARROW_RIGHT"
if seq[1] == 'D': return "ARROW_LEFT"
if seq[1] == 'H': return "HOME_KEY"
if seq[1] == 'F': return "END_KEY"
elif seq[0] == 'O':
if seq[1] == 'H': return "HOME_KEY"
if seq[1] == 'F': return "END_KEY"
return 'ESC'
return c
def main():
# Save terminal settings
fd = sys.stdin.fileno()
old_tty_settings = termios.tcgetattr(fd)
# Enter raw mode
tty.setraw(sys.stdin)
################################################################################
interrupt = controlKey("c")
while True:
s = readKey()
if s:
print(f"{s}", end="\r\n")
if s == interrupt:
break
################################################################################
# Exit raw mode
fd = sys.stdin.fileno()
termios.tcsetattr(fd, termios.TCSADRAIN, old_tty_settings)
if __name__ == "__main__":
main()
Upvotes: 0
Views: 90
Reputation: 13079
If you use low-level I/O, I think it works. select.select
will accept numerical file descriptors. I haven't tried to integrate this with your program, but have a play with this. You should get a sequence of characters if you press e.g. left arrow. The original seems not to work with sys.stdin
, but this is fine with fd 0. Note the os.read
to read from numerical file descriptor.
import os
import sys
import select
import tty
import termios
def read_all_available(fd):
"do a single blocking read plus non-blocking reads while any more data exists"
if not select.select([fd],[],[], None)[0]:
return None
val = os.read(fd, 1)
while select.select([fd],[],[], 0)[0]:
val += os.read(fd, 1)
return val
data = None
while data != b'\x03':
old_settings = termios.tcgetattr(0)
tty.setraw(sys.stdin)
data = read_all_available(0)
# reset settings here just to allow tidier printing to screen
termios.tcsetattr(0, termios.TCSADRAIN, old_settings)
print(data, len(data))
Upvotes: 1