Reputation: 61
I need a timed input for python 3 on both Mac and Windows which, if the user runs out of time, automatically chooses one of the options. Every other answer for a question I've seen only terminates after user presses 'Enter'. I need it to terminate regardless of any 'Enter' pressed or not.
For example, in my chess program, if no input is given after 5 seconds, I need it to automatically choose "Q". Also if the input is not in ["Q", "R", "B", "N"] then I also need it to choose "Q".
The following code doesn't work. It'll ask for the input then wait forever; the timer doesn't work at all.
def countdown():
global timer
timer = 5
for x in range(timer):
timer -= 1
time.sleep(1)
countdownThread = threading.Thread(target=countdown)
countdownThread.start()
while timer > 0:
promotedPiece = input("Promote to Q, R, B or N? > ").upper()
if timer == 0:
break
if timer == 0:
if promotedPiece not in ["Q", "R", "B", "N"]: # If not valid input, choose Queen
promotedPiece = "Q"
print("Queen auto selected")
Upvotes: 6
Views: 685
Reputation: 1117
I've spent a few hours, and this is as far as I got:
import time
from multiprocessing import Process
global timer
timer = 5
def get_answer():
answer = input ("ANSWER THE QUESTION!!!")
def runtime():
global timer
while True:
time.sleep(1)
timer = timer-1
print (timer)
if timer == 0:
thread1.terminate()
thread1.join()
print ("hi")
break
# create two new threads
thread1 = Process(target=get_answer, args=())
thread2 = Process(target=runtime, args=())
# start the threads
thread1.start()
thread2.start()
# wait for the threads to complete
thread1.join()
thread2.join()
All you need to do is work out the error I'm getting with the Process
module, and you'll be good.
Upvotes: 0
Reputation: 3345
If you're comfortable with async
, it's a very simple solution to this problem, with the aioconsole
(pip install aioconsole
) library:
import asyncio
from aioconsole import ainput # async version of input()
async def get_choice() -> str:
try:
coro = ainput("Promote to Q, R, B or N? > ")
choice = await asyncio.wait_for(coro, timeout=5.0)
except asyncio.TimeoutError:
print() # go to next line past ">" of prompt
choice = "Q"
return choice.upper()
async def main():
print("You chose", await get_choice())
asyncio.run(main())
Upvotes: 2
Reputation: 1533
As managing input in different environments is quite different, it is a good idea to use a module where someone else has taken care of it. pynput
does what you want. Be careful as it has some caveats.
Here is the implementation of timeout_input
and timeout_getchar
functions that are good enough for general use. In your case, timeout_getchar
should be a perfect match:
from pynput import keyboard
from time import monotonic
def timeout_input(timeout:float, prompt:str='', echo=True, None_on_timeout=False):
"""Read string with timeout.
Reads keypresses until enter is pressed or timeout occurs. On enter,
returns the characters read. On timeout the return value depends on the
None_on_timeout parameter.
Parameters
----------
timeout : float
Timeout in seconds
prompt : str
Prompt to print
echo : bool
If True, echo keys to screen as they are pressed
None_on_timeout : bool
If this parameter is True and timeout is reached, None is returned.
Otherwise a (possibly empty) str of characters read so far is returned
Returns
-------
str or None
Characters read
Observations
------------
Requires pynput. All caveats of pynput apply to this function. As it
records keypresses some differences with plain input behavior are expected.
"""
rv = []
print(prompt, end='')
with keyboard.Events() as events:
while timeout > 0:
start_time = monotonic()
event = events.get(timeout)
if event is not None:
if isinstance(event, keyboard.Events.Press):
try:
if isinstance(event.key.char, str):
if echo:
print(event.key.char, end='')
rv.append(event.key.char)
except AttributeError:
if event.key == keyboard.Key.enter:
return ''.join(rv)
timeout -= monotonic() - start_time
return None if None_on_timeout else ''.join(rv)
def timeout_getchar(timeout:float, prompt:str=''):
"""Read a single char with timeout.
On timeout the return value is None
Parameters
----------
timeout : float
Timeout in seconds
prompt : str
Prompt to print
Returns
-------
str or None
Character read
Observations
------------
Requires pynput. All caveats of pynput apply to this function. As it
records keypresses some differences with plain input behavior are expected
"""
print(prompt, end='')
with keyboard.Events() as events:
while timeout > 0:
start_time = monotonic()
event = events.get(timeout)
if event is not None:
if isinstance(event, keyboard.Events.Press):
try:
if isinstance(event.key.char, str):
return event.key.char
except AttributeError:
pass
timeout -= monotonic() - start_time
return None
promotedPiece = timeout_getchar(5, "Promote to Q, R, B or N? > ")
print()
if promotedPiece is None:
print("You took too long. I choose Q for you")
promotedPiece = 'Q'
promotedPiece = promotedPiece.upper()
if promotedPiece not in ["Q", "R", "B", "N"]: # If not valid input, choose Queen
print(f"I said Q, R, B or N. You can not choose {promotedPiece}. I choose Q for you")
promotedPiece = 'Q'
print("selection = ", promotedPiece)
Upvotes: 0
Reputation: 2179
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:
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 ''
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}")
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
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:
pygame.event.wait(timeout in milliseconds)
)cv2.waitKey(delay in milliseconds)
)Then you can set a variable promoting
and if that is true, return the pressed key or an alternative key after the wait period.
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).
Upvotes: 7