Skip to content
Advertisement

How to key press detection on a Linux terminal, low level style in python

I just implemented a Linux command shell in python using only the os library’s low level system calls, like fork() and so on.

I was wondering how I can implement a key listener that will listen for key (UP|DOWN) to scroll through the history of my shell.

I want do do this without using any fancy libraries, but I am also wishing that this is not something super complicated. My code is just about 100 lines of code, so far, and I don’t want to create a monster just to get a simple feature 😀

My thoughts about the problem is, that it should be possible to create a child process with some kind of loop, that will listen for up ^[[A and down ^[[B, key press, and then somehow put the text into my input field, like a normal terminal.

So far the thing I am most interested in is the possibility of the key-listener. But next I will probably have to figure out how I will get that text into the input field. About that I am thinking that I probably have to use some of the stdin features that sys provides.

I’m only interested in making it work on Linux, and want to continue using low-level system calls, preferably not Python libraries that handle everything for me. This is a learning exercise.

Advertisement

Answer

By default the standard input is buffered and uses canonical mode. This allows you to edit your input. When you press the enter key, the input can be read by Python.

If you want a lower level access to the input you can use tty.setraw() on the standard input file descriptor. This allows you to read one character at a time using sys.stdin.read(1). Note that in this case the Python script will be responsible for handling special characters, and you will lose some of the functionality like character echoing and deleting. For more information take a look at termios(3).

You can read about escape sequences which are used for up and down keys on Wikipedia.

You should be able to replicate the standard shell behavior if you handle everything in one process.

You may also want to try using a subprocess (not referring to the module – you can use fork() or popen()). You would parse the unbuffered input in the main process and send it to stdin (which can be buffered) of the subprocess. You will probably need to have some inter-process communication to share history with the main process.

Here is an example of the code needed to capture the input this way. Note that it is only doing some basic processing and needs more work in order to fit your use-case.

import sys
import tty
import termios


def getchar():
    fd = sys.stdin.fileno()
    attr = termios.tcgetattr(fd)
    try:
        tty.setraw(fd)
        return sys.stdin.read(1)
    finally:
        termios.tcsetattr(fd, termios.TCSANOW, attr)


EOT = 'x04'  # CTRL+D
ESC = 'x1b'
CSI = '['

line = ''

while True:
    c = getchar()
    if c == EOT:
        print('exit')
        break
    elif c == ESC:
        if getchar() == CSI:
            x = getchar()
            if x == 'A':
                print('UP')
            elif x == 'B':
                print('DOWN')
    elif c == 'r':
        print([line])
        line = ''
    else:
        line += c
User contributions licensed under: CC BY-SA
5 People found this is helpful
Advertisement