getch() und kbhit()

Code-Stücke können hier veröffentlicht werden.
Antworten
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

Hier mal eine "plattformunabhängige" getch(), getwch() und kbhit()-Implementation.

Wie sich unixnahe Exoten wie RiscOS etc. verhalten, weiß ich nicht, getestet ist es nur unter Linux.

Vielleicht hat auch jemand einen besseren Vorschlag für die Unicode-Dekodiererei, das gefällt mir so garnicht, scheint aber erstmal zu funktionieren. Auch der Umweg über einen extra Puffer ist nicht das Gelbe vom Ei, gibt es da eine Möglichkeit, die Daten im Lesepuffer des Terminals zu halten?

Code
ms4py
User
Beiträge: 1178
Registriert: Montag 19. Januar 2009, 09:37

jerch hat geschrieben: Vielleicht hat auch jemand einen besseren Vorschlag für die Unicode-Dekodiererei, das gefällt mir so garnicht, scheint aber erstmal zu funktionieren.
Falls Python2.6 kannst du mal das versuchen:

Code: Alles auswählen

from __future__ import unicode_literals
BlackJack

@ice2k3: Und was soll das bringen!? Ich sehe da keine Unicode-*Literale* in dem Quelltext.
Dauerbaustelle
User
Beiträge: 996
Registriert: Mittwoch 9. Januar 2008, 13:48

Ist auf Windows-Systemen aber ungünstig, weil die Posix-Klasse trotzdem abgearbeitet wird. Das ist unnötig, mach das doch alle auf der Ebene des `except`s.

Gruß
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

@Dauerbaustelle:
Naja, mir gings weniger um Modul-Schönheit und wenn ich nicht den Import des sys-Moduls im Windowsteil vergessen hätte, würde es auch funktionieren :oops:
Für eine echte Umsetzung würde ich wahrscheinlich eher den Posixkram auslagern und über einen Import analog zum Windowsimport einbinden.

Hab auch grad gesehen, dass im getwch() Fehler drin sind, werde es bei Gelegenheit nochmal überarbeiten.
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

Hier eine etwas bereinigte Version.

__init__.py

Code: Alles auswählen

try:
    from msvcrt import getch, getwch, kbhit
except ImportError:
    from posix_term import _PosixTerm
    _posix_term = _PosixTerm()
    getch = _posix_term.getch
    getwch = _posix_term.getwch
    kbhit = _posix_term.kbhit
posix_term.py

Code: Alles auswählen

import sys, os, tty, termios, fcntl

class _PosixTerm(object):
    def __init__(self):
        self.peek_buffer = {}

    def _read_blocking(self, fd):
        term_settings = termios.tcgetattr(fd)
        try:
            tty.setcbreak(fd, termios.TCSANOW)
            char = os.read(fd, 1)
        finally:
            termios.tcsetattr(fd, termios.TCSANOW, term_settings)
        return char

    def _read_nonblocking(self, fd):
        term_settings = termios.tcgetattr(fd)
        flags = fcntl.fcntl(fd, fcntl.F_GETFL)
        try:
            fcntl.fcntl(fd, fcntl.F_SETFL, flags | os.O_NONBLOCK)
            tty.setcbreak(fd, termios.TCSANOW)
            char = os.read(fd, 1)
        except OSError: char = ""
        finally:
            fcntl.fcntl(fd, fcntl.F_SETFL, flags)
            termios.tcsetattr(fd, termios.TCSANOW, term_settings)
        return char

    def kbhit(self, fd=sys.stdin.fileno()):
        if self.peek_buffer.get(fd):
            return 1
        char = self._read_nonblocking(fd)
        self.peek_buffer[fd] = char
        return int(bool(char))

    def getch(self, fd=sys.stdin.fileno()):
        if self.peek_buffer.get(fd):
            char = self.peek_buffer[fd][0]
            self.peek_buffer[fd] = self.peek_buffer[fd][1:]
            return char
        return self._read_blocking(fd)

    def getwch(self, fd=sys.stdin.fileno(), encoding=sys.stdin.encoding):
        raw_str = ""
        for i in xrange(4):
            if self.peek_buffer.get(fd):
                raw_str += self.peek_buffer[fd][0]
                self.peek_buffer[fd] = self.peek_buffer[fd][1:]
            else:
                if raw_str:
                    raw_char = self._read_nonblocking(fd)
                    if not raw_char: break
                    raw_str += raw_char
                else:
                    raw_str = self._read_blocking(fd)
            try:
                u_char = unicode(raw_str, encoding)
                return u_char
            except UnicodeDecodeError: continue
        return unicode(raw_str, encoding)
Funktioniert jetzt auch wieder unter Windows :D

getwch() ist nach wie vor unglücklich, nur wußte ich auf die Schnelle keinen besseren Weg, die variable Bytelänge der verschiedenen Kodierungen zu verarbeiten. Ein komplexerer Ansatz mit Auswertung der Kodierungseigenschaften etc. wäre hier wahrscheinlich robuster, liegt allerdings jenseits meines Interesses bezüglich dieses kleinen Helferleins.

Edit: Rückgabe von kbhit() liefert nicht True/False sondern 1/0 - verbessert.
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

Eine weitere Version, diesmal mit völlig anderem Ansatz. kbhit() verwendet nun poll(), dadurch fällt der Puffer weg, die Terminalbehandlung erfolgt über einen Kontextmanager wodurch ein einfacher Kontextwechsel zwischen raw terminal und line terminal mittels with möglich werden soll.

Mein aktuelles Problem ist, daß ich nicht weiß, wie man an die systemweite Grundkonfiguration für den kanonischen Terminalmodus kommt, ohne gleich das Terminal zurückzusetzen (also die Info, die 'reset' bzw. 'tput reset' auswertet), weshalb ich im Moment davon ausgehe, das der erste Aufruf von TerminalHandler() das Terminal im Ausgangszustand vorfindet, was ja nicht unbedingt stimmen muß.

Prinzipiell besser wäre es, das Terminal nach dem with-Block in den vorherigen Zustand zurückzusetzen, aber auch hierfür fehlt mir die Information für den line-Modus (bzw. habe ich nur obige). Für den raw-Modus ist das kein Problem, da das tty-Modul hierfür vorgefertigte Funktionen anbietet.

Hat da vielleicht jemand eine Lösung?

Hier der Code (Achtung experimenteller Bastelcode mit wirren Paradigmensprüngen ;) )
Benutzeravatar
snafu
User
Beiträge: 6855
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Ich würde die beiden Modi als Konstanten auf Modulebene definieren.

Beim Encoding würde ich auch Strings vermeiden. Man könnte die Konstanten in [mod]codecs[/mod] mappen. Oder verwechsel ich hier gerade was?
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

snafu hat geschrieben:Ich würde die beiden Modi als Konstanten auf Modulebene definieren.
Naja, das geht leider nicht, da ja erst zur "Laufzeit" der file descriptor bekannt ist, welcher z.B. auch eine serielle Konsole oder ähnliches sein kann:

Code: Alles auswählen

with raw_terminal(<nicht stdin-terminal-fd>):
    # usw.
Das würde dann nicht mehr funktionieren.
snafu hat geschrieben: Beim Encoding würde ich auch Strings vermeiden. Man könnte die Konstanten in [mod]codecs[/mod] mappen. Oder verwechsel ich hier gerade was?
Ja da kommt wieder stdin.encoding rein.

@lunar:
Ich nehme an, snafu meint, die kanonischen Einstellungen einfach als Konstanten vorbelegen lassen.
Benutzeravatar
snafu
User
Beiträge: 6855
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

@Lunar:
The module also provides the following constants which are useful for reading and writing to platform dependent files:

codecs.BOM
codecs.BOM_BE
codecs.BOM_LE
codecs.BOM_UTF8
codecs.BOM_UTF16
codecs.BOM_UTF16_BE
codecs.BOM_UTF16_LE
codecs.BOM_UTF32
codecs.BOM_UTF32_BE
codecs.BOM_UTF32_LE
These constants define various encodings of the Unicode byte order mark (BOM) used in UTF-16 and UTF-32 data streams to indicate the byte order used in the stream or file and in UTF-8 as a Unicode signature. BOM_UTF16 is either BOM_UTF16_BE or BOM_UTF16_LE depending on the platform’s native byte order, BOM is an alias for BOM_UTF16, BOM_LE for BOM_UTF16_LE and BOM_BE for BOM_UTF16_BE. The others represent the BOM in UTF-8 and UTF-32 encodings.
Ist aber was anderes, oder?
jerch hat geschrieben:@lunar:
Ich nehme an, snafu meint, die kanonischen Einstellungen einfach als Konstanten vorbelegen lassen.
Ja, die meinte ich. Ich dachte auf dem ersten Blick nur, die gibt es im `codecs`-Modul, damit man die Möglichkeiten nicht einzeln definieren muss.
Zuletzt geändert von snafu am Freitag 23. Oktober 2009, 17:21, insgesamt 1-mal geändert.
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

@snafu:
Falls es Dich interessiert, google doch mal nach cooked mode, rare mode und raw mode. Das Problem hat nix mit dem encoding zu tun, sondern mit dem Verhalten des Terminals.
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

Update:
Ich habe versucht, nach dem Vorbild der tty-Funktionen eine eigene Funktion für den normalen Zeilenmodus zu basteln:

Code: Alles auswählen

from termios import *

IFLAG = 0
OFLAG = 1
CFLAG = 2
LFLAG = 3
ISPEED = 4
OSPEED = 5

def setcooked(fd, when=TCSAFLUSH):
    """Put terminal into cooked mode."""
    mode = tcgetattr(fd)
    mode[IFLAG] = BRKINT | ICRNL | INPCK | ISTRIP | IXON | IGNPAR
    mode[OFLAG] = OPOST | ONLCR
    mode[CFLAG] = mode[CFLAG] | CS8
    mode[LFLAG] = ECHOKE | ECHOCTL | ECHOK | ECHOE | ECHO | ICANON | IEXTEN | ISIG
    tcsetattr(fd, when, mode)
Die Werte sind größtenteils von meiner Standardbitmaske abgeschrieben. Für einzelne Werte ist mir die Bedeutung nicht klar, daher ist das Ganze ziemlich fehlerträchtig, vielleicht kann da nochmal jemand mit mehr Terminalerfahrung drüber schauen.

Mit Überarbeitung des Kontextmanagers sieht es jetzt so aus: Code
(Das hard-reset fliegt wieder raus)
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

Hier mal eine etwas entrümpelte Posix-Version (setcooked() siehe oben):

Code: Alles auswählen

import os
import sys
from termios import TCSANOW, TCSADRAIN, TCSAFLUSH, tcsetattr, tcgetattr
from functools import partial
from select import poll, POLLIN
from tty import setcbreak, setraw
from tty_extension import setcooked

# TODO: comments

TERM_MODES = {'cooked': setcooked, 'cbreak': setcbreak, 'raw': setraw}

class TerminalHandler(object):
    _cooked_settings = {}
    def __new__(cls, *args, **kwargs):
        instance = object.__new__(cls)
        instance.cooked_settings = cls._cooked_settings
        return instance

    def __init__(self,
                 mode='cbreak',
                 fd=sys.stdin.fileno(),
                 when=TCSADRAIN,
                 when_exit=TCSAFLUSH):
        self.mode = mode
        self.fd = fd
        self.when = when
        self.when_exit = when_exit
        self.old_settings = tcgetattr(self.fd)
        self.cooked_settings.setdefault(self.fd, self.old_settings)

    def __enter__(self):
        TERM_MODES[self.mode](self.fd, self.when)
        return self.fd

    def __exit__(self, type_, value, traceback):
        tcsetattr(self.fd, self.when_exit, self.old_settings)

    @classmethod
    def reset(cls, fd=sys.stdin.fileno()):
        if cls._cooked_settings.get(fd):
            tcsetattr(fd, TCSAFLUSH, cls._cooked_settings[fd])

cooked_terminal = partial(TerminalHandler, 'cooked')
cbreak_terminal = partial(TerminalHandler, 'cbreak')
rare_terminal = cbreak_terminal
raw_terminal = partial(TerminalHandler, 'raw')
reset_terminal = TerminalHandler.reset

def _kbhit():
    poll_obj = poll()
    def wrapped(fd=sys.stdin.fileno(), timeout=0):
        poll_obj.register(fd, POLLIN)
        with cbreak_terminal(fd, when_exit=TCSADRAIN):
            for i in poll_obj.poll(timeout):
                if i[0] == fd and i[1] & POLLIN:
                    return 1
        return 0
    return wrapped

kbhit = _kbhit()

def getch(fd=sys.stdin.fileno()):
    with cbreak_terminal(fd, when_exit=TCSADRAIN) as rterm:
        return os.read(rterm, 1)

def getwch(fd=sys.stdin.fileno(), encoding=sys.stdin.encoding):
    raw_str = ""
    for i in xrange(4):
        if raw_str:
            if kbhit():
                raw_str += getch(fd)
            else: break
        else:
            raw_str = getch(fd)
        try:
            return unicode(raw_str, encoding)
        except UnicodeDecodeError: continue
    return unicode(raw_str, encoding)
Der Kontextmanager stellt nun die Terminalmodi cooked, cbreak und raw bereit (als shortcuts cooked_terminal(), cbreak_terminal() und raw_terminal()). Bei Verlassen des with-Blockes wird jetzt der vorherige Zustand restauriert, mit reset_terminal() gibt es noch die Möglichkeit, das Terminal direkt auf den Ausgangszustand zurückzusetzen.
Antworten