This is a very interesting question, and many people have been looking at timed inputs:
About the retrieving of a single character very good answers are provided for all platforms:
Solutions
Linux
The general consensus is that when you are using Linux
the following works good (taken from jer's answer):
import signal
class AlarmException(Exception):
pass
def alarmHandler(signum, frame):
raise AlarmException
def nonBlockingRawInput(prompt='', timeout=20):
signal.signal(signal.SIGALRM, alarmHandler)
signal.alarm(timeout)
try:
text = raw_input(prompt)
signal.alarm(0)
return text
except AlarmException:
print '\nPrompt timeout. Continuing...'
signal.signal(signal.SIGALRM, signal.SIG_IGN)
return ''
Windows
But when you are on Windows, there is no real good solution. People have been trying to use threads and interrupt signals, but when I tried them out I couldn't get them working properly. Most direct solutions are summarized in this gist from atupal
. But there are many more attempts:
The last thing I could come up with for your specific situation is to simulate a key press after the timeout. This key press will then be the user input (even though the user didn't input one, for the program it seems like he did).
For this solution I created the following files:
listener.py
: with the code from https://stackoverflow.com/a/510364/10961342
class _Getch:
"""Gets a single character from standard input. Does not echo to the
screen."""
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, sys
def __call__(self):
import sys, 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()
getch = _Getch()
keys.py
: with (unicode) from https://stackoverflow.com/a/13290031/10961342
(from the question: How to generate keyboard events?)
import ctypes
import time
SendInput = ctypes.windll.user32.SendInput
PUL = ctypes.POINTER(ctypes.c_ulong)
KEYEVENTF_UNICODE = 0x0004
KEYEVENTF_KEYUP = 0x0002
class KeyBdInput(ctypes.Structure):
_fields_ = [("wVk", ctypes.c_ushort),
("wScan", ctypes.c_ushort),
("dwFlags", ctypes.c_ulong),
("time", ctypes.c_ulong),
("dwExtraInfo", PUL)]
class HardwareInput(ctypes.Structure):
_fields_ = [("uMsg", ctypes.c_ulong),
("wParamL", ctypes.c_short),
("wParamH", ctypes.c_ushort)]
class MouseInput(ctypes.Structure):
_fields_ = [("dx", ctypes.c_long),
("dy", ctypes.c_long),
("mouseData", ctypes.c_ulong),
("dwFlags", ctypes.c_ulong),
("time", ctypes.c_ulong),
("dwExtraInfo", PUL)]
class Input_I(ctypes.Union):
_fields_ = [("ki", KeyBdInput),
("mi", MouseInput),
("hi", HardwareInput)]
class Input(ctypes.Structure):
_fields_ = [("type", ctypes.c_ulong),
("ii", Input_I)]
def PressKey(KeyUnicode):
extra = ctypes.c_ulong(0)
ii_ = Input_I()
ii_.ki = KeyBdInput(0, KeyUnicode, KEYEVENTF_UNICODE, 0, ctypes.pointer(extra))
x = Input(ctypes.c_ulong(1), ii_)
ctypes.windll.user32.SendInput(1, ctypes.pointer(x), ctypes.sizeof(x))
def ReleaseKey(KeyUnicode):
extra = ctypes.c_ulong(0)
ii_ = Input_I()
ii_.ki = KeyBdInput(0, KeyUnicode, KEYEVENTF_UNICODE | KEYEVENTF_KEYUP, 0, ctypes.pointer(extra))
x = Input(ctypes.c_ulong(1), ii_)
ctypes.windll.user32.SendInput(1, ctypes.pointer(x), ctypes.sizeof(x))
def PressAltTab():
PressKey(0x012) # Alt
PressKey(0x09) # Tab
time.sleep(2) # optional : if you want to see the atl-tab overlay
ReleaseKey(0x09) # ~Tab
ReleaseKey(0x012) # ~Alt
if __name__ == "__main__":
PressAltTab()
main.py
: with the following
import time
import multiprocessing as mp
import listener
import keys
def press_key(delay=2, key='Q'):
time.sleep(delay)
# All keys https://learn.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes?redirectedfrom=MSDN
keys.PressKey(int(hex(ord(key)), base=16)) # 0x51
keys.ReleaseKey(int(hex(ord(key)), base=16)) # 0x51
if __name__ == '__main__':
process = mp.Process(target=press_key)
process.start()
question = "Promote to?"
choice = ('Q', 'B', 'R', 'N')
print(f"Promote to? {choice}\n>>> ", end='')
reply = (listener.getch()).upper().decode('utf-8')
process.terminate()
if reply not in choice:
reply = 'Q'
print(f"Promoted piece to {reply}")
Explanation
listener
makes sure to only listen to a single key stroke, while keys
gives us the option to simulate a user key. The main
calls a Process that will automatically send a Q
to the stdin after a certain delay
in seconds. A process has been made so we can call process.terminate()
before the Q
is send, if we have a user reply (much more complicated with a thread).
Notes
This only works if you directly read from the cmd console. If you use an IDE, you have to emulate the terminal. For example in Pycharm, you have to select Emulate terminal in output console
in Run/Debug Configurations
.
The terminal window has to be selected, otherwise the Q
will be send to whatever window is currently active.
Promote to? ('Q', 'B', 'R', 'N')
>>> Promoted piece to Q
Process finished with exit code 0
Promote to? ('Q', 'B', 'R', 'N')
>>> Promoted piece to N
Process finished with exit code 0
Gui alternatives
Even though it is hard to make it work directly in python, there are alternative solutions when you take the whole program into account. The question seems to refer to chess, and when you build a full game (with gui) you can often use their build in keyboard capture. For example:
Then you can set a variable promoting
and if that is true, return the pressed key or an alternative key after the wait period.
Edit
A minimal working example in pygame
would be:
import pygame
def set_timer(timeout=5):
print(f"To which piece do you want to promote?")
pygame.time.set_timer(pygame.USEREVENT, timeout * 1_000)
return True
def check_timer_keys(event):
if event.type == pygame.KEYDOWN:
if event.unicode.upper() in ['Q', 'R', 'N', 'B']:
print(f'Key press {event}')
return False
else:
print(f"Invalid key! {event}")
return True
if event.type == pygame.USEREVENT:
print("Exceeded time, user event")
return False
return True
if __name__ == '__main__':
pygame.init()
screen = pygame.display.set_mode((500, 500))
running = True
timer = False
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
# Should be before the T call, other wise it also handles the `t` call.
if timer:
timer = check_timer_keys(event)
# When pressing T, start the promotion question.
if event.type == pygame.KEYDOWN:
if event.unicode.upper() == 'T':
timer = set_timer(timeout=2)
screen.fill((255, 255, 255))
pygame.display.flip()
pygame.quit()
Whenever you press the t
key a timer start to run with a 2 second timeout. This means that after 2 seconds you will get a custom pygame.USEREVENT
, which means that the user failed to answer within the time. In the check_timer_keys
I filter for the Q, R, N and B
key, hence other keys are not processed for this specific question.
A current limitation is that you can mix the USEREVENT
call if you promote multiple pieces within the timeout
period. If you want to prevent this, you have to add a counter that indicates how many USEREVENT
's call are estill being processed (if number is larger than 1, wait until it is 1).