Alexis Lamotte
Alexis Lamotte

Reputation: 53

How to intercept keys pressed in (and only in) the terminal window of a Python program?

I'm writing a simple Python 3 script and I want to be able to intercept key presses inside the terminal window in order to do something depending on the returned value, for example.
I also want a cross-platform solution.

I would like to reproduce something like this:

import msvcrt
key = ord(msvcrt.getch()) # Wait for a key to be pressed.
if key == 27: # The ESC key
    print("You have pressed the ESC key!")

But msvcrt is a Windows-specific module according to the Python docs (and my tests):

These functions provide access to some useful capabilities on Windows platforms.

I've found the keyboard module which is quite simple to use (and more cross-platform) but I didn't manage to "catch" only the keys pressed inside the terminal window.

For example:

import keyboard as kb
key = kb.read_hotkey()
if key == "esc": # The ESC key
    print("You have pressed the ESC key!")

The code given above intercepts key presses not only when the terminal window where the script is executed is focused, but also when it is not.

So, to conclude, do you know a pythonic way to intercept key presses inside the terminal window (and not outside) where the script is executed (something like an input() without having to press Enter), and which is cross-platform (at least compatible with GNU/Linux and Windows)?

Thank you in advance for your answers,
Regards,
Alexis.

Upvotes: 4

Views: 3498

Answers (2)

tryexceptcontinue
tryexceptcontinue

Reputation: 1877

Have a look at the curses module. It's in the python standard library, but does not support windows out of the box. There is a regularly maintained project called "windows-curses" you can have a look at. I have not tested it, but it supposedly will allow you to use the python curses module on windows. https://pypi.org/project/windows-curses/

import curses

def listen(window):
    while True:
        key = window.getch()
        window.addstr(f'You pressed the "{key}" key!\n')
        if key == 'q':
            break
        handle_keypress(key)

curses.wrapper(listen)

If the curses approach doesn't work for you, or you still need a bit more granularity, then you can roll your own cross-platform approach fairly easily. You can try something like this:

from sys import platform

class Keyboard:
    def __new__(cls):
        if platform in ['Windows', 'win32', 'cygwin']:
            cls = winKeyboard
        elif platform in ['Mac', 'darwin', 'os2', 'os2emx']:
            cls = MacKeyboard
        else:
            raise Exception(f'Unrecognized platform, {platform}')
        return super(Keyboard, cls).__new__(cls)

    def listen(self):
        while True:
            key = self.getch()
            print(f'You pressed the "{key}" key!')
            if key == 27:
                break
            return self.handle_key(key)

class MacKeyboard(Keyboard):
    def getch(self):
        implement_mac_logic()


class WinKeyboard(Keyboard):
    def getch(self):
        implement_win_logic()

keyboard = Keyboard()
keyboard.listen()

Keyboard.__new__ does the work of providing the appropriate solution at runtime for the current os.

This approach will still register key-presses regardless of the active window.
In order to do this, you will need access to the active window, which will be another os-specific procedure. Have a look at this: https://stackoverflow.com/a/36419702/1420455

You could implement a function that checked the name of the current window

class Keyboard:
    ...
    def listen(self):
        while True:
            if self.get_active_window() != desired_window:
                continue
            key = self.getch()
            print(f'You pressed the "{key}" key!')
            if key == 27:
                break
            return self.handle_key(key)

Then you can just implement the approate logic in WinKeyboard.get_active_window and MacKeyboard.get_active_window This wont take into account being in different tabs. This may be possible, but I am not familiar enough with the apis to tell you.

There are also options such as pygame that will require you to create and manage your own windows but will meet your requirements.

Edit: Changed WinKeyboard and MacKeyboard to inherit from Keyboard.

Upvotes: 1

Alexis Lamotte
Alexis Lamotte

Reputation: 53

This is a partial solution that works on Windows and would work on GNU/Linux.
I noticed that on GNU/Linux (at least on Debian 9) the same number is assigned to the arrow keys and the ESC key.

For the code below, I was inspired by the solution of this subject.

# coding: utf8
import sys


def read() -> int:
    if sys.platform == "win32":
        import msvcrt
        key = ord(msvcrt.getch())  # Wait for a key to be pressed.
    elif sys.platform == "linux":
        import tty
        import termios
        try:
            orig_settings = termios.tcgetattr(sys.stdin)
            tty.setcbreak(sys.stdin)
            key = ord(sys.stdin.read(1)[0])
        except KeyboardInterrupt:
            key = 3 # The code for Ctrl+C got on Windows.
        finally:  # To make sure that the terminal will return to its original state.
            termios.tcsetattr(sys.stdin, termios.TCSADRAIN, orig_settings)
    else:
        raise RuntimeError("Your platform is not supported")
    return key

if read() == 27:  # The ESC key (also the UP-DOWN-RIGHT-LEFT on GNU/Linux)
    print("You have pressed the ESC key!")

Regards,
Alexis.

Upvotes: 1

Related Questions