problem with escape sequence on linux for getch python

372 Views Asked by At

I have a pretty big problem, I'm making an _input function in python, and for that I need a getch function, so I started coding this, but I have a big problem: the escape sequences on linux, their size is variable! So I would like to know how to detect these sequences properly (and quickly).

import os
import sys

if os.name == "nt":
    import msvcrt
else:
    import tty
    import termios

_special_key_tn = {
    "H"   : "A"  , #top
    "P"   : "B"  , #down
    "M"   : "C"  , #right
    "K"   : "D"  , #left
    "S"   : "?"  , #suppr
    "R"   : "_"  , #insert
    "O"   : "!"  , #end
}

_special_key = {
    "A"   : "A"  , #top
    "B"   : "B"  , #down
    "C"   : "C"  , #right
    "D"   : "D"  , #left
    "3"   : "?"  , #suppr
    "2"   : "_"  , #insert
    "F"   : "!"  , #end
}

def _getch() :
    """
        Attend la frappe d'un caractère puis le retourne
        @returns:
            ch {str}
    """

    sys.stdout.flush()
    ch = ""
    if os.name == "nt" :
        ch = msvcrt.getwch()
    else :
        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)

    #if os.name == "nt" :
    #    if ord(ch) == 224 :
    #        ch = "^[" + _special_key_tn[_getch()]
    #else :
    #    if ord(ch) == 27 :
    #        _getch()
    #        ch = "^[" + _special_key[_getch()]
    #        print(_getch())

    if os.name == "nt" and msvcrt.kbhit() :
        print("ok")

    return ch

if __name__ == "__main__":
    print("key pressed :", _getch())
    print("key pressed :", _getch())
    print("key pressed :", _getch())
    print("key pressed :", _getch())
    print("key pressed :", _getch())
    print("key pressed :", _getch())

(the _input function but it has no problem at the moment)

class color :
    def list() :
        for i in range(100) :
            print(f"\033[1;{i}m{i}")
    white       = lambda string: "\033[1;37m" + string + "\033[0;00m"
    grey        = lambda string: "\033[1;30m" + string + "\033[0;00m"
    yellow      = lambda string: "\033[1;33m" + string + "\033[0;00m"
    green       = lambda string: "\033[1;32m" + string + "\033[0;00m"
    blue        = lambda string: "\033[1;34m" + string + "\033[0;00m"
    cyan        = lambda string: "\033[1;36m" + string + "\033[0;00m"
    red         = lambda string: "\033[1;31m" + string + "\033[0;00m"
    magenta     = lambda string: "\033[1;35m" + string + "\033[0;00m"
    black       = lambda string: "\033[1;30m" + string + "\033[0;00m"
    darkwhite   = lambda string: "\033[0;37m" + string + "\033[0;00m"
    darkyellow  = lambda string: "\033[0;33m" + string + "\033[0;00m"
    darkgreen   = lambda string: "\033[0;32m" + string + "\033[0;00m"
    darkblue    = lambda string: "\033[0;34m" + string + "\033[0;00m"
    darkcyan    = lambda string: "\033[0;36m" + string + "\033[0;00m"
    darkred     = lambda string: "\033[0;31m" + string + "\033[0;00m"
    darkmagenta = lambda string: "\033[0;35m" + string + "\033[0;00m"
    darkblack   = lambda string: "\033[0;30m" + string + "\033[0;00m"
    placeholder = lambda string: "\033[1;30m" + string + "\033[0;00m" + f"\033[{len(string)}D"

def _input(string="", end="\n", pos=(0, 0), placeholder="", max=-1, width=-1, background="") :
    enc   = "utf-8"
    line  = ""
    i     = 0
    start = 0

    if width == -1 :
        width = tuple(os.get_terminal_size())[0] - 2
    width -= len(string) + pos[0]
    stop  = f"\033[{width+1}C"
    placeholder = placeholder[:width]

    y_pos = f"\033[{pos[1]}E" if pos[1] != 0 else ""
    print(f"{y_pos}\033[{pos[0] + 1}G\0337\033[0G{background}\0338{string}◄{color.placeholder(placeholder)}\0338{stop}►\0338◄", end="")
    while True :
        sys.stdout.flush()
        char = msvcrt.getwch()

        if char in ["\n", "\r", "\n\r", "\r\n"] :
            # entry
            break
        if char == "\x08" :
            # backspace
            if i != 0 :
                temp = len(line)
                line = line[:i-1] + line[i:]
                i -= temp - len(line)
            char = ""
        if char == '\xe0' :
            # arrows
            mov = msvcrt.getwch()
            if mov == "K" : # left
                if i > 0 :
                    i -= 1
            if mov == "M" : # right
                if i < len(line) :
                    i += 1
            if mov == "S" : # suppr
                if len(line) - i != 0 :
                    temp = len(line)
                    line = line[:i] + line[i+1:]
            char = ""
        if max != -1 and len(line) >= max :
            # limit lenght
            char = ""

        line = line[:i] + char + line[i:]
        i += len(char)

        if i > width :
            width += 1
            start += 1
        elif i < start :
            start -= 1
            width -= 1

        right = left = 0
        if width < len(line) :
            right = 1
        if start > 0 :
            left = 1

        j = f"\033[{i-start+1}C" if i-start+1 != 0 else ""
        l = color.green("◄") if left  else "◄"
        r = color.green("►") if right else "►"
        ph = color.placeholder(placeholder) if len(line) == 0 else ""
        print(f"\033[2K\033[0G{background}\0338{string}{l}{ph}{line[start:width]}\0338{stop}{r}\0338{j}", end="")

    print(end, end="")
    return line

PS: If you have ideas for optimizations for my functions, give them, because on my pc it runs almost well, but I doubt that it runs well on all PCs.

2

There are 2 best solutions below

1
Manolo On

indeed, these are sequences ANSI, but I did not know them and they were not in the doc I had (gist.github.com/fnky/458719343aabd01cfb17a3a4f7296797). Now I have another problem: When I type "only" esc, the program waits for another input. How can I check if there are still "pending entries"? (I tried sys.stdin but found it didn't work...

My new code:

import os
import sys

if os.name == "nt":
    import msvcrt
else:
    import tty
    import termios

_escapes_sequences_tn = {
    "H"   : "A"  , #top
    "P"   : "B"  , #down
    "M"   : "C"  , #right
    "K"   : "D"  , #left
}

def _getch() :
    """
        Attend la frappe d'un caractère puis le retourne
        @returns:
            ch {str}
    """

    sys.stdout.flush()
    ch = ""
    if os.name == "nt" :
        ch = msvcrt.getwch()
    else :
        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)

    if os.name == "nt" :
        if ord(ch) == 224 :
            ch = "^[" + _escapes_sequences_tn[_getch()]
    else :
        if ord(ch) == 27 :
            _ch = _getch()
            if len(_ch) == 1 and ord(_ch) == 91 :
                _ch += _getch()

                while _ch[-1].isnumeric() :
                    _ch += _getch()
            else :
                _ch += _getch()

            ch = "^[" + _ch

    return ch

if __name__ == "__main__":
    print("key pressed :", _getch())
    print("key pressed :", _getch())
    print("key pressed :", _getch())
    print("key pressed :", _getch())
    print("key pressed :", _getch())
    print("key pressed :", _getch())
0
Manolo On

This is a possible solution :

import os
import sys

if os.name == "nt":
    import msvcrt
else:
    import tty
    import termios

class _keyboard_nt:
    TOP         = "^[H"    # top
    DOWN        = "^[P"    # down
    RIGHT       = "^[M"    # right
    LEFT        = "^[K"    # left
    DEL         = "^[S"    # del
    END         = "^[O"    # end
    NL          = "\r"     # newline
    TAB         = "\t"     # tab
    BS          = "\x08"   # backspace
    ctrl_A      = "\x01"   # ctrl+A
    ctrl_Z      = "\x1a"   # ctrl+Z
    ctrl_E      = "\x05"   # ctrl+E
    ctrl_R      = "\x12"   # ctrl+R
    ctrl_T      = "\x14"   # ctrl+T
    ctrl_Y      = "\x19"   # ctrl+Y
    ctrl_U      = "\x15"   # ctrl+U
    ctrl_I      = "\t"     # ctrl+I
    ctrl_O      = "\x0f"   # ctrl+O
    ctrl_P      = "\x10"   # ctrl+P
    ctrl_Q      = "\x11"   # ctrl+Q
    ctrl_S      = "\x13"   # ctrl+S
    ctrl_D      = "\x04"   # ctrl+D
    ctrl_F      = "\x06"   # ctrl+F
    ctrl_G      = "\x07"   # ctrl+G
    ctrl_H      = "\x08"   # ctrl+H
    ctrl_J      = "\n"     # ctrl+J
    ctrl_K      = "\x0b"   # ctrl+K
    ctrl_L      = "\x0c"   # ctrl+L
    ctrl_M      = "\r"     # ctrl+M
    ctrl_W      = "\x17"   # ctrl+W
    ctrl_X      = "\x18"   # ctrl+X
    ctrl_C      = "\x03"   # ctrl+C
    ctrl_B      = "\x02"   # ctrl+B
    ctrl_N      = "\x0e"   # ctrl+N
    ctrl_UP     = "^[\x8d" # ctrl+UP
    ctrl_DOWN   = "^[\x91" # ctrl+DOWN
    ctrl_LEFT   = "^[s"    # ctrl+LEFT
    ctrl_RIGHT  = "^[t"    # ctrl+RIGHT
    ctrl_NL     = "\n"     # ctrl+newline
    ctrl_BS     = "\x7f"   # ctrl+backspace
    ctrl_AST    = "\x1c"   # ctrl+*
    ctrl_DOLLAR = "\x1d"   # ctrl+$
    def ctrl(char) :
        assert type(char) == str, "char is not a str"
        ctrl_char = {
            "\x01" : "A", "\x1a" : "Z", "\x05" : "E", "\x12" : "R",
            "\x14" : "T", "\x19" : "Y", "\x15" : "U", "\t"   : "I",
            "\x0f" : "O", "\x10" : "P", "\x11" : "Q", "\x13" : "S",
            "\x04" : "D", "\x06" : "F", "\x07" : "G", "\x08" : "H",
            "\n"   : "J", "\x0b" : "K", "\x0c" : "L", "\r"   : "M",
            "\x17" : "W", "\x18" : "X", "\x03" : "C", "\x02" : "B",
            "\x0e" : "N",
            "^[\x8d": "UP"  , "^[\x91": "DOWN" ,
            "^[s"   : "LEFT", "^[t"   : "RIGHT",
            "\n"    : "NL"  , "\x7f"  : "BS"   ,
            "\x1c" : "*", "\x1d" : "$",
        }
        return ctrl_char.get(char)

class _keyboard_ln:
    TOP         = "^[[A"    # top
    DOWN        = "^[[B"    # down
    RIGHT       = "^[[C"    # right
    LEFT        = "^[[D"    # left
    DEL         = "^[[3~"   # del
    END         = "^[[F"    # end
    NL          = "\r"      # newline
    TAB         = "\t"      # tab
    BS          = "\x7f"    # backspace
    ctrl_A      = "\x01"    # ctrl+A
    ctrl_Z      = "\x1a"    # ctrl+Z
    ctrl_E      = "\x05"    # ctrl+E
    ctrl_R      = "\x12"    # ctrl+R
    ctrl_T      = "\x14"    # ctrl+T
    ctrl_Y      = "\x19"    # ctrl+Y
    ctrl_U      = "\x15"    # ctrl+U
    ctrl_I      = "\t"      # ctrl+I
    ctrl_O      = "\x0f"    # ctrl+O
    ctrl_P      = "\x10"    # ctrl+P
    ctrl_Q      = "\x11"    # ctrl+Q
    ctrl_S      = "\x13"    # ctrl+S
    ctrl_D      = "\x04"    # ctrl+D
    ctrl_F      = "\x06"    # ctrl+F
    ctrl_G      = "\x07"    # ctrl+G
    ctrl_H      = "\x08"    # ctrl+H
    ctrl_J      = "\n"      # ctrl+J
    ctrl_K      = "\x0b"    # ctrl+K
    ctrl_L      = "\x0c"    # ctrl+L
    ctrl_M      = "\r"      # ctrl+M
    ctrl_W      = "\x17"    # ctrl+W
    ctrl_X      = "\x18"    # ctrl+X
    ctrl_C      = "\x03"    # ctrl+C
    ctrl_B      = "\x02"    # ctrl+B
    ctrl_N      = "\x0e"    # ctrl+N
    ctrl_UP     = "^[[1;5A" # ctrl+UP
    ctrl_DOWN   = "^[[1;5B" # ctrl+DOWN
    ctrl_LEFT   = "^[[1;5D" # ctrl+LEFT
    ctrl_RIGHT  = "^[[1;5C" # ctrl+RIGHT
    ctrl_NL     = "\n"      # ctrl+newline
    ctrl_BS     = "\x08"    # ctrl+backspace
    ctrl_AST    = "\x1c"    # ctrl+*
    ctrl_DOLLAR = "\x1d"    # ctrl+$
    def ctrl(char) :
        assert type(char) == str, "char is not a str"
        ctrl_char = {
            "\x01" : "A", "\x1a" : "Z", "\x05" : "E", "\x12" : "R",
            "\x14" : "T", "\x19" : "Y", "\x15" : "U", "\t"   : "I",
            "\x0f" : "O", "\x10" : "P", "\x11" : "Q", "\x13" : "S",
            "\x04" : "D", "\x06" : "F", "\x07" : "G", "\x08" : "H",
            "\n"   : "J", "\x0b" : "K", "\x0c" : "L", "\r"   : "M",
            "\x17" : "W", "\x18" : "X", "\x03" : "C", "\x02" : "B",
            "\x0e" : "N",
            "^[[1;5A": "UP"  , "^[[1;5B": "DOWN" ,
            "^[[1;5D": "LEFT", "^[[1;5C": "RIGHT",
            "\n"     : "NL"  , "\x08"   : "BS"   ,
            "\x1c" : "*", "\x1d" : "$",
        }
        return ctrl_char.get(char)

def _getch_nt() :
    """
        Attend la frappe d'un caractère puis le retourne (pour Windows)
        @returns:
            ch {str}
    """

    sys.stdout.flush()
    ch = ""

    ch = msvcrt.getwch()

    if ord(ch) == 224 :
        ch = "^[" + getch()

    return ch

def _getch_ln() :
    """
        Attend la frappe d'un caractère puis le retourne (pour Linux)
        @returns:
            ch {str}
    """

    sys.stdout.flush()
    ch = ""

    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)

    if ord(ch) == 27 :
        _ch = getch()
        _ch += getch()

        while _ch[-1].isnumeric() or _ch[-1] == ";" :
            _ch += getch()

        ch = "^[" + _ch

    return ch

if os.name == "nt" :
    getch = _getch_nt
    class keyboard(_keyboard_nt):
        pass
else :
    getch = _getch_ln
    class keyboard(_keyboard_ln):
        pass

if __name__ == "__main__":
    ch = ""
    while ch != keyboard.END :
        ch = getch()
        print("key pressed :", keyboard.ctrl(ch))