2013-04-13 15 views
9

Ho bisogno di una funzione che legge l'input in un buffer come raw_input(), ma invece di eseguire l'input e il blocco dell'eco fino alla restituzione di una riga completa, dovrebbe supress echo e invoca una richiamata ogni volta che il buffer cambia.Python, modifica riga "filtrata", leggere stdin per carattere senza eco

Io dico "modifica buffer" anziché "carattere letto" perché, come raw_input(), mi piacerebbe che fosse a conoscenza di chiavi speciali. Backspace dovrebbe funzionare, per esempio.

Se avessi voluto, ad esempio, utilizzare il callback per simulare eco maiuscolo dell'ingresso, il codice sarebbe simile a questa:

def callback(text): 
    print '\r' + text.upper() 

read_input(callback) 

Come posso raggiungere questo obiettivo?

NOTA: Ho cercato di utilizzare readline e curses incontrare i miei fini, ma entrambi i binding Python sono incompleti. Non è possibile avviare curses senza svuotare l'intero schermo e readline offre un singolo hook prima dell'inizio di qualsiasi input.

risposta

10

Bene, ho scritto il codice a mano. Lascerò una spiegazione per riferimento futuro.

Requisiti

import sys, tty, termios, codecs, unicodedata 
from contextlib import contextmanager 

disabilitazione buffering di riga

Il primo problema che sorge quando semplicemente leggendo stdin è linea buffer. Vogliamo singoli personaggi per raggiungere il nostro programma senza una nuova riga richiesta, e questo non è il modo predefinito in cui il terminale opera.

Per questo, ho scritto un manager contesto che gestisce tty configurazione:

@contextmanager 
def cbreak(): 
    old_attrs = termios.tcgetattr(sys.stdin) 
    tty.setcbreak(sys.stdin) 
    try: 
     yield 
    finally: 
     termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_attrs) 

Questo Manager consente il seguente linguaggio:

with cbreak(): 
    single_char_no_newline = sys.stdin.read(1) 

E 'importante eseguire la pulizia quando abbiamo finito oppure il terminale potrebbe richiedere un reset.

decodifica stdin

Il secondo problema con solo leggendo stdin è la codifica. I caratteri unicode non-ascii ci raggiungeranno byte per byte, il che è completamente indesiderabile.

Per decodificare correttamente stdin, ho scritto un generatore che possiamo iterare per i caratteri unicode:

def uinput(): 
    reader = codecs.getreader(sys.stdin.encoding)(sys.stdin) 
    with cbreak(): 
     while True: 
      yield reader.read(1) 

Questo può venire a mancare sopra i tubi. Non ne sono sicuro. Per il mio caso d'uso, tuttavia, raccoglie la giusta codifica e genera un flusso di caratteri.

Manipolazione caratteri speciali

Prima di tutto, dovremmo essere in grado di dire caratteri stampabili oltre a quelli di controllo:

def is_printable(c): 
    return not unicodedata.category(c).startswith('C') 

parte stampabili, per ora, voglio solo per gestire ← backspace e la CtrlD sequenza:

def is_backspace(c): 
    return c in ('\x08','\x7F') 

def is_interrupt(c): 
    return c == '\x04' 

Mettere insieme: xinput()

Ora è tutto a posto. Il contratto originale per la funzione che volevo era input di lettura, gestire caratteri speciali, richiamare la callback. L'implementazione riflette proprio questo:

def xinput(callback): 
    text = '' 

    for c in uinput(): 
     if is_printable(c): text += c 
     elif is_backspace(c): text = text[:-1] 
     elif is_interrupt(c): break 

     callback(text) 

    return text 

provarlo

def test(text): 
    print 'Buffer now holds', text 

xinput(test) 

L'esecuzione e digitando Hellx← backspaceo mondiale spettacoli:

Buffer now holds H 
Buffer now holds He 
Buffer now holds Hel 
Buffer now holds Hell 
Buffer now holds Hellx 
Buffer now holds Hell 
Buffer now holds Hello 
Buffer now holds Hello 
Buffer now holds Hello w 
Buffer now holds Hello wo 
Buffer now holds Hello wor 
Buffer now holds Hello worl 
Buffer now holds Hello world