Hlavní navigace

Tvorba aplikací s textovým uživatelským rozhraním založeným na knihovně prompt_toolkit

12. 7. 2018
Doba čtení: 26 minut

Sdílet

Ve druhém článku o knihovně prompt_toolkit se budeme zabývat pokročilejšími nastaveními řádky. Ukážeme si zápis víceřádkového textu, použití externího editoru, validaci dat při jejich zadávání a nástrojové pruhy (TUI).

Obsah

1. Tvorba aplikací s textovým uživatelským rozhraním založeným na knihovně prompt_toolkit

2. Základní funkcionalita pro interaktivní aplikace – vstupní (textový) řádek

3. Podpora pro zápis víceřádkového textu

4. Zpracování stisku Ctrl+C a Ctrl+D

5. Validace textu zapisovaného uživatelem po stisku klávesy Enter

6. Validace již v průběhu zadávání vstupních údajů

7. Povolení použití externího textového editoru po použití zkratky Ctrl+X Ctrl+E nebo v

8. Spodní nástrojový pruh a zpráva zapsaná na pravém okraji výzvy

9. Přístup k editovanému textu (objekty Buffer a Document)

10. Callback funkce volané během editace

11. Demonstrační příklad: spodní nástrojový pruh a zpráva na pravém okraji výzvy reagující na uživatelský vstup

12. Změna stylu zobrazení všech relevantních prvků TUI

13. Zvýraznění syntaxe textu zapisovaného uživatelem na vstup

14. Vytvoření vlastního lexeru s klíčovými slovy

15. Demonstrační příklad používající programátorem definovaný lexer

16. Použití myši na vstupním textovém řádku

17. Repositář s demonstračními příklady

18. Odkazy na Internetu

1. Tvorba aplikací s textovým uživatelským rozhraním založeným na knihovně prompt_toolkit

Na předchozí článek s úvodními informacemi o knihovnách GNU Readline a prompt_toolkit dnes navážeme. Popíšeme si totiž další možnosti, které programátorům (a vlastně také uživatelům) knihovna prompt_toolkit nabízí při zdánlivě primitivní operaci – zápisu textových dat přes terminál (konzoli). Ve skutečnosti se u mnoha aplikací jedná o nejdůležitější prvek jejich uživatelského rozhraní, takže se programátoři snaží své aplikace vylepšovat a zadávání dat co nejvíce zjednodušit a vylepšit. A právě v tomto ohledu nabízí knihovna prompt_toolkit poměrně velké množství funkcí, které jsou navíc jednoduše použitelné (obdobnou funkcionalitu je mnohdy v plnohodnotných GUI knihovnách dosti těžké dosáhnout).

Všechny dále popsané příklady jsou založeny na skriptu využívajícího objekt typu PromptSession. který jsme si již popsali minule:

from prompt_toolkit import PromptSession
 
 
def show_help():
    print("""Help
--------
quit - quit this application
exit - exit from this application
eval - evaluate
""")
 
 
s = PromptSession()
 
while True:
    cmd = s.prompt("Command: ")
    if cmd in {"q", "quit", "Quit", "exit", "Exit"}:
        break
    elif cmd in {"help", "Help", "?"}:
        show_help()
    elif cmd == "eval":
        print("42")

2. Základní funkcionalita pro interaktivní aplikace – vstupní (textový) řádek

Mezi funkce, které je možné díky existenci knihovny prompt_toolkit využít při programování vstupního řádku, patří zejména:

3. Podpora pro zápis víceřádkového textu

V některých aplikacích se setkáme s nutností vstupu víceřádkového textu (představme si například textový editor, který je součástí mailového klienta typu Mutt). Tento režim je knihovnou prompt_toolkit samozřejmě taktéž podporován a pro jeho použití postačuje do metody PromptSession.prompt() předat nepovinný parametr multiline:

s = PromptSession()
 
user_input = s.prompt("Command: ", multiline=True)

Na následujících screenshotech je ukázáno chování takto nakonfigurovaného příkazového řádku:

Obrázek 1: Po stisku klávesy Enter nedojde k ukončení vstupu, ale „pouze“ k přechodu na další řádek. Mezi řádky se můžete přesouvat kurzorovými šipkami, řádky lze spojovat apod.

Obrázek 2: Vstup je ukončen stiskem kombinace Esc, Enter. Metoda prompt() vrátí jediný řetězec, v němž jsou řádky odděleny znaky \n.

Obrázek 3: Pozor na to, že znak pro nový řádek (\n) je součástí vráceného řetězce.

Obrázek 4: Příkaz „quit“ je nutné zapsat posloupností kláves q, u, i, t, Esc, Enter.

Výše popsané chování je naprogramováno v dnešním prvním demonstračním příkladu:

from prompt_toolkit import PromptSession
 
 
def show_help():
    print("""Help
--------
quit - quit this application
exit - exit from this application
eval - evaluate
""")
 
 
s = PromptSession()
 
while True:
    cmd = s.prompt("Command: ", multiline=True)
    print("Entered text: {}".format(cmd))
    if cmd in {"q", "quit", "Quit", "exit", "Exit"}:
        break
    elif cmd in {"help", "Help", "?"}:
        show_help()
    elif cmd == "eval":
        print("42")

4. Zpracování stisku Ctrl+C a Ctrl+D

Při čekání na zápis textu uživatelem do příkazového řádku je nutné myslet na speciální význam kláves Ctrl+C a Ctrl+D. Stisk Ctrl+C totiž ve výchozím nastavení vede k vyvolání výjimky typu KeyboardInterrupt a stisk klávesy Ctrl+D na prázdném řádku ke vzniku výjimky typu EOFError (pokud řádek prázdný není, slouží tato klávesa jako alternativa za Delete).

Ukončení aplikace pro stisku Ctrl+D:

Traceback (most recent call last):
  File "prompt10_multiline_edit.py", line 16, in <module>
    cmd = s.prompt("Command: ", multiline=True)
  File "/home/tester/.local/lib/python3.4/site-packages/prompt_toolkit/shortcuts/prompt.py", line 722, in prompt
    return run_sync()
  File "/home/tester/.local/lib/python3.4/site-packages/prompt_toolkit/shortcuts/prompt.py", line 706, in run_sync
    return self.app.run(inputhook=self.inputhook)
  File "/home/tester/.local/lib/python3.4/site-packages/prompt_toolkit/application/application.py", line 682, in run
    return run()
  File "/home/tester/.local/lib/python3.4/site-packages/prompt_toolkit/application/application.py", line 656, in run
    return f.result()
  File "/home/tester/.local/lib/python3.4/site-packages/prompt_toolkit/eventloop/future.py", line 149, in result
    raise self._exception
  File "/home/tester/.local/lib/python3.4/site-packages/prompt_toolkit/eventloop/coroutine.py", line 90, in step_next
    new_f = coroutine.throw(exc)
  File "/home/tester/.local/lib/python3.4/site-packages/prompt_toolkit/application/application.py", line 635, in _run_async2
    result = yield f
  File "/home/tester/.local/lib/python3.4/site-packages/prompt_toolkit/eventloop/coroutine.py", line 92, in step_next
    new_f = coroutine.send(f.result())
  File "/home/tester/.local/lib/python3.4/site-packages/prompt_toolkit/application/application.py", line 596, in _run_async
    result = yield From(f)
EOFError

Ukončení aplikace pro stisku Ctrl+C:

Traceback (most recent call last):
  File "prompt10_multiline_edit.py", line 16, in
    cmd = s.prompt("Command: ", multiline=True)
  File "/home/tester/.local/lib/python3.4/site-packages/prompt_toolkit/shortcuts/prompt.py", line 722, in prompt
    return run_sync()
  File "/home/tester/.local/lib/python3.4/site-packages/prompt_toolkit/shortcuts/prompt.py", line 706, in run_sync
    return self.app.run(inputhook=self.inputhook)
  File "/home/tester/.local/lib/python3.4/site-packages/prompt_toolkit/application/application.py", line 682, in run
    return run()
  File "/home/tester/.local/lib/python3.4/site-packages/prompt_toolkit/application/application.py", line 656, in run
    return f.result()
  File "/home/tester/.local/lib/python3.4/site-packages/prompt_toolkit/eventloop/future.py", line 149, in result
    raise self._exception
  File "/home/tester/.local/lib/python3.4/site-packages/prompt_toolkit/eventloop/coroutine.py", line 90, in step_next
    new_f = coroutine.throw(exc)
  File "/home/tester/.local/lib/python3.4/site-packages/prompt_toolkit/application/application.py", line 635, in _run_async2
    result = yield f
  File "/home/tester/.local/lib/python3.4/site-packages/prompt_toolkit/eventloop/coroutine.py", line 92, in step_next
    new_f = coroutine.send(f.result())
  File "/home/tester/.local/lib/python3.4/site-packages/prompt_toolkit/application/application.py", line 596, in _run_async
    result = yield From(f)
KeyboardInterrupt

V případě, že tyto výjimky nejsou odchyceny, bude aplikace ihned ukončena, což nemusí být vždy žádoucí (opět si představme například násilně ukončenou aplikaci typu GNU Octave s rozpracovaným projektem). Obě výjimky lze samozřejmě snadno zachytit, což je ukázáno v dalším příkladu i s typickými reakcemi – Ctrl+C násilně ukončí stávající příkaz (a očekává příkaz nový), Ctrl+D ukončí celou smyčku příkazů a pokračuje v další činnosti aplikace):

from prompt_toolkit import PromptSession
 
 
def show_help():
    print("""Help
--------
quit - quit this application
exit - exit from this application
eval - evaluate
""")
 
 
s = PromptSession()
 
while True:
    try:
        cmd = s.prompt("Command: ")
        if cmd in {"q", "quit", "Quit", "exit", "Exit"}:
            break
        elif cmd in {"help", "Help", "?"}:
            show_help()
        elif cmd == "eval":
            print("42")
    except KeyboardInterrupt:
        continue
    except EOFError:
        break

5. Validace textu zapisovaného uživatelem po stisku klávesy Enter

Prakticky ve všech aplikacích je nutné text zapsaný uživatelem nějakým způsobem zvalidovat a popř. nahlásit chybu. I tuto funkcionality je možné naprogramovat a to dokonce dvěma způsoby, které se od sebe odlišují uživatelskou přívětivostí a nároky na výpočetní výkon. První způsob spočívá v tom, že se validace provede až po odeslání textu klávesou Enter. Samotná validace je implementována objektem typu Validator, který obsahuje metodu validate. Tato metoda je zavolána automaticky a pokud nevyhodí výjimku ValidationError, je vstup zpracován běžným způsobem. V opačném případě se vypíše chybová zpráva a kurzor je umístěn na místo chyby – oba tyto údaje se předávají právě ve výjimce ValidationError:

Obrázek 5: Způsob zobrazení chybové zprávy a přesun kurzoru na místo s chybným znakem. Při vzniku chyby se příkaz neodešle, pouze se čeká na jeho opravu.

Obrázek 6: Až po zápisu korektního příkazu je provedeno jeho zpracování.

Ukažme si jednoduchý validátor, který testuje, jestli uživatel zapsal příkaz složený z písmen abecedy. Ve výjimce je předána jak chybová zpráva, tak i index prvního špatného znaku:

class CommandValidator(Validator):
 
    def validate(self, document):
        user_input = document.text
 
        if user_input and not user_input.isalpha():
            index = 0
 
            for index, char in enumerate(user_input):
                if not char.isalpha():
                    break
 
            msg = "Wrong character '{c}' on index {i}".format(c=char, i=index)
            raise ValidationError(message=msg, cursor_position=index)

Validátor se povolí následovně:

s = PromptSession()
 
cmd = s.prompt("Command: ", validator=CommandValidator(), validate_while_typing=False)

Následuje výpis úplného zdrojového kódu tohoto příkladu:

from prompt_toolkit.validation import Validator, ValidationError
from prompt_toolkit import PromptSession
 
 
def show_help():
    print("""Help
--------
quit - quit this application
exit - exit from this application
eval - evaluate
""")
 
 
class CommandValidator(Validator):
 
    def validate(self, document):
        user_input = document.text
 
        if user_input and not user_input.isalpha():
            index = 0
 
            for index, char in enumerate(user_input):
                if not char.isalpha():
                    break
 
            msg = "Wrong character '{c}' on index {i}".format(c=char, i=index)
            raise ValidationError(message=msg, cursor_position=index)
 
 
s = PromptSession()
 
while True:
    try:
        cmd = s.prompt("Command: ", validator=CommandValidator(), validate_while_typing=False)
        if cmd in {"q", "quit", "Quit", "exit", "Exit"}:
            break
        elif cmd in {"help", "Help", "?"}:
            show_help()
        elif cmd == "eval":
            print("42")
    except KeyboardInterrupt:
        continue
    except EOFError:
        break

6. Validace již v průběhu zadávání vstupních údajů

Druhá metoda validace spočívá v tom, že se text zadávaný uživatelem kontroluje průběžně, tj. po jakékoli editační operaci, která do textu přidá znaky, smaže znaky nebo je prohodí (Ctrl+T) se zavolá validátor. Průběžná kontrola může být uživatelsky přívětivější (aplikace je plně kooperativní), ovšem pokud je validátor implementován komplikovanějším algoritmem, je náročnější na zdroje (čas CPU):

s = PromptSession()
 
cmd = s.prompt("Command: ", validator=CommandValidator(), validate_while_typing=True)

Při „realtime“ validaci se zobrazuje chybové hlášení, ovšem neprovádí se posun kurzoru:

Obrázek 7: „Realtime“ validace textu zapisovaného uživatelem.

Opět se podívejme na úplný zdrojový kód tohoto příkladu:

from prompt_toolkit.validation import Validator, ValidationError
from prompt_toolkit import PromptSession
 
 
def show_help():
    print("""Help
--------
quit - quit this application
exit - exit from this application
eval - evaluate
""")
 
 
class CommandValidator(Validator):
 
    def validate(self, document):
        user_input = document.text
 
        if user_input and not user_input.isalpha():
            index = 0
 
            for index, char in enumerate(user_input):
                if not char.isalpha():
                    break
 
            msg = "Wrong character '{c}' on index {i}".format(c=char, i=index)
            raise ValidationError(message=msg, cursor_position=index)
 
 
s = PromptSession()
 
while True:
    try:
        cmd = s.prompt("Command: ", validator=CommandValidator(), validate_while_typing=True)
        if cmd in {"q", "quit", "Quit", "exit", "Exit"}:
            break
        elif cmd in {"help", "Help", "?"}:
            show_help()
        elif cmd == "eval":
            print("42")
    except KeyboardInterrupt:
        continue
    except EOFError:
        break

7. Povolení použití externího textového editoru po použití zkratky Ctrl+X Ctrl+E nebo v

I přesto, že prompt_toolkit dokáže poměrně uspokojivě emulovat chování textových editorů Vi/Vim či Emacs, nejedná se o plnohodnotnou náhradu těchto aplikací (zvláště ve chvíli, kdy má uživatel svůj textový editor nastavený podle vlastních požadavků). Navíc někteří uživatelé mohou preferovat jiný textový editor, ať již běžící v terminálu či s plnohodnotným GUI. Podobně, jako je tomu na příkazové řádce, lze i při použití knihovny prompt_toolkit použít klávesovou zkratku Ctrl+X Ctrl+E (režim Emacs) nebo v (režim Vi) pro spuštění externího textového editoru, kterému se předá dočasný soubor obsahující text již zadaný na příkazové řádce. Po ukončení editoru je text předán na příkazovou řádku, jakoby ho tam uživatel přímo zapsal. Pro docílení této funkcionality je pouze nutné použít nepovinný parametr enable_open_in_editor:

s = PromptSession()
 
cmd = s.prompt("Command: ", enable_open_in_editor=True)

Chování aplikace bude vypadat následovně:

Obrázek 8: Editace textu přímo v aplikaci.

Obrázek 9: Vyvolání externího editoru, v němž se automaticky zobrazí již dříve zapsaný text. Dočasný soubor s textem se po ukončení textového editoru automaticky smaže.

Editor, který se má spustit, se specifikuje v proměnné prostředí EDITOR, například:

export EDITOR=vim
Poznámka: pokud nastavíte editor s GUI, je nutné ověřit, že vše bude funkční, tj. zda aplikace pozná, kdy se editor ukončil. Například pro Gvim (Vim s GUI) je nutné použít EDITOR=„gvim -f“ (spuštění na popředí).

Opět se podívejme, jak vypadá úplná implementace celého skriptu. Od předchozích příkladů se bude odlišovat pouze v povolení použití externího textového editoru:

from prompt_toolkit.validation import Validator, ValidationError
from prompt_toolkit import PromptSession
 
 
def show_help():
    print("""Help
--------
quit - quit this application
exit - exit from this application
eval - evaluate
""")
 
 
class CommandValidator(Validator):
 
    def validate(self, document):
        user_input = document.text
 
        if user_input and not user_input.isalpha():
            index = 0
 
            for index, char in enumerate(user_input):
                if not char.isalpha():
                    break
 
            msg = "Wrong character '{c}' on index {i}".format(c=char, i=index)
            raise ValidationError(message=msg, cursor_position=index)
 
 
s = PromptSession()
 
while True:
    try:
        cmd = s.prompt("Command: ", validator=CommandValidator(),
                       validate_while_typing=True,
                       enable_open_in_editor=True)
        if cmd in {"q", "quit", "Quit", "exit", "Exit"}:
            break
        elif cmd in {"help", "Help", "?"}:
            show_help()
        elif cmd == "eval":
            print("42")
    except KeyboardInterrupt:
        continue
    except EOFError:
        break

8. Spodní nástrojový pruh a zpráva zapsaná na pravém okraji výzvy

Již v předchozím článku jsme se zmínili o tom, že knihovnu prompt_toolkit je možné využít i pro tvorbu aplikací s celoobrazovkovým textovým uživatelským rozhraním (TUI). Velmi jednoduché TUI nám nabízí i funkce prompt popř. metoda PromptSession.prompt, protože umožňuje definovat nástrojový pruh zobrazený na spodní části obrazovky popř. zprávu zobrazenou na pravém okraji vstupního řádku. Tyto dva prvky uživatelského rozhraní mohou buď zobrazovat neměnnou zprávu uživateli, nebo dokonce mohou reagovat na zapisovaný text (což je vlastnost, kterou si ukážeme v další kapitole). Nejprve se podívejme na způsob nastavení konstantních (neměnných) zpráv. K tomuto účelu se používají nepovinné parametry bottom_toolbar a rprompt, kterým předáme řetězce popř. proměnné obsahující řetězce:

s = PromptSession()
 
cmd = s.prompt("Command: ", validator=CommandValidator(),
               validate_while_typing=True,
               enable_open_in_editor=True,
               bottom_toolbar="Available commands: quit, exit, help, eval",
               rprompt="Don't panic!")

Podívejme se nyní na to, jakým způsobem se vlastně oba dva nové prvky textového uživatelského rozhraní zobrazí:

Obrázek 10: Nástrojový pruh umístěný v dolní části terminálu a zpráva vypsaná na pravém okraji výzvy (vstupního řádku).

Další chování TUI při editaci vstupního řádku:

Obrázek 11: Současné zobrazení výzvy, editovaného textu, nástrojového pruhu, zprávy na pravém okraji výzvy a chybového hlášení při zápisu nekorektního znaku.

Úplný kód skriptu, v němž jsou použity oba dva nové prvky textového uživatelského rozhraní, vypadá následovně a získat ho můžete z adresy https://github.com/tisnik/pre­sentations/blob/master/prom­pt_toolkit/prompt/prompt15_bot­tom_toolbar.py:

from prompt_toolkit.validation import Validator, ValidationError
from prompt_toolkit import PromptSession
 
 
def show_help():
    print("""Help
--------
quit - quit this application
exit - exit from this application
eval - evaluate
""")
 
 
class CommandValidator(Validator):
 
    def validate(self, document):
        user_input = document.text
 
        if user_input and not user_input.isalpha():
            index = 0
 
            for index, char in enumerate(user_input):
                if not char.isalpha():
                    break
 
            msg = "Wrong character '{c}' on index {i}".format(c=char, i=index)
            raise ValidationError(message=msg, cursor_position=index)
 
 
s = PromptSession()
 
while True:
    try:
        cmd = s.prompt("Command: ", validator=CommandValidator(),
                       validate_while_typing=True,
                       enable_open_in_editor=True,
                       bottom_toolbar="Available commands: quit, exit, help, eval",
                       rprompt="Don't panic!")
        if cmd in {"q", "quit", "Quit", "exit", "Exit"}:
            break
        elif cmd in {"help", "Help", "?"}:
            show_help()
        elif cmd == "eval":
            print("42")
    except KeyboardInterrupt:
        continue
    except EOFError:
        break

9. Přístup k editovanému textu (objekty Buffer a Document)

V některých situacích, například při implementaci dále popsaných callback funkcí volaných během editace příkazového řádku, je nutné získat text, který již byl uživatelem zapsán. Předpokládejme, že máme k dispozici instanci třídy PromptSession a nacházíme se v režimu editace:

s = PromptSession()
cmd = s.prompt("Command: ")

V jakékoli callback funkci máme přístup ke globální proměnné s a můžeme využít dále popsanou uživatelskou funkci get_user_input pro získání již zapsaného textu. Tato funkce pracuje následovně: nejdříve z instance třídy PromptSession získá výchozí buffer (to je instance třídy Buffer) a z tohoto objektu pak přečte atribut document, což je pro změnu instance třídy Document. Instance této třídy obsahují atribut text, což je kýžený řetězec (měl by vždy existovat, i když může obsahovat prázdný řetězec):

def get_user_input(prompt_session):
    # nejprve ziskame objekt typu Buffer
    buffer = prompt_session.default_buffer
 
    # z bufferu ziskame objekt typu Document
    document = buffer.document
 
    # ktery obsahuje atribut 'text'
    return document.text

10. Callback funkce volané během editace

Nyní, když víme, jak je možné prakticky kdykoli získat text zapsaný uživatelem na vstupní (příkazový) řádek, již můžeme vytvořit aktivní spodní nástrojový pruh i návěští na pravé straně vstupního řádku (aktivní ve smyslu, že jejich obsah bude reagovat na text zadávaný uživatelem). K tomuto účelu se používají callback funkce specifikované opět v nepovinných parametrech bottom_toolbar a rprompt. V předchozím demonstračním příkladu jsme v těchto parametrech předávali řetězec, ale alternativně můžeme předat i referenci na callback funkci (rozlišení se provede v čase běhu aplikace – runtime):

cmd = s.prompt("Command: ", validator=CommandValidator(),
               validate_while_typing=True,
               enable_open_in_editor=True,
               bottom_toolbar=bottom_toolbar_callback,
               rprompt=right_prompt_callback)

Návěští na pravé straně vstupního řádku bude průběžně zobrazovat celkový počet zapsaných znaků, který zjistíme jednoduše standardní funkcí len:

def right_prompt_callback():
    user_input = get_user_input(s)
    return "Typed {c} characters".format(c=len(user_input))

Ve spodním nástrojovém pruhu se buď zobrazí informace o korektně zapsaném příkazu, nebo pouze lakonická zpráva „???“ ve chvíli, kdy není příkaz rozpoznán:

def bottom_toolbar_callback():
    user_input = get_user_input(s)
    if user_input in {"quit", "exit", "eval", "help"}:
        return "Valid command, press Enter"
    else:
        return "???"
Poznámka: samozřejmě si můžete obě callback funkce libovolně upravit; uvádím je zde zejména z toho důvodu, aby se ukázaly všechny možnosti nabízené knihovnou prompt_toolkit.

11. Demonstrační příklad: spodní nástrojový pruh a zpráva na pravém okraji výzvy reagující na uživatelský vstup

Obě výše popsané callback funkce jsou společně s funkcí get_user_input použity v dalším demonstračním příkladu, jehož úplný zdrojový kód naleznete na adrese https://github.com/tisnik/pre­sentations/blob/master/prom­pt_toolkit/prompt/prompt16_ca­llback_functions.py. Podívejme se na chování příkladu při editaci:

Obrázek 12: Pokud je příkazový řádek prázdný, zobrazuje se nulová délka a současně zpráva „???“ (neznámý příkaz).

Obrázek 13: Současné zobrazení chybového hlášení společně s dalšími prvky TUI.

Obrázek 14: Pokud uživatel napíše validní příkaz, dozví se o tom mj. i ze zprávy zobrazené na spodním toolbaru.

Zdrojový kód příkladu:

from prompt_toolkit.validation import Validator, ValidationError
from prompt_toolkit import PromptSession
 
 
def show_help():
    print("""Help
--------
quit - quit this application
exit - exit from this application
eval - evaluate
""")
 
 
class CommandValidator(Validator):
 
    def validate(self, document):
        user_input = document.text
 
        if user_input and not user_input.isalpha():
            index = 0
 
            for index, char in enumerate(user_input):
                if not char.isalpha():
                    break
 
            msg = "Wrong character '{c}' on index {i}".format(c=char, i=index)
            raise ValidationError(message=msg, cursor_position=index)
 
 
def get_user_input(prompt_session):
    # nejprve ziskame objekt typu Buffer
    buffer = prompt_session.default_buffer
    # z bufferu ziskame objekt typu Document
    document = buffer.document
    # ktery obsahuje atribut 'text'
    return document.text
 
 
def right_prompt_callback():
    user_input = get_user_input(s)
    return "Typed {c} characters".format(c=len(user_input))
 
 
def bottom_toolbar_callback():
    user_input = get_user_input(s)
    if user_input in {"quit", "exit", "eval", "help"}:
        return "Valid command, press Enter"
    else:
        return "???"
 
 
s = PromptSession()
 
while True:
    try:
        cmd = s.prompt("Command: ", validator=CommandValidator(),
                       validate_while_typing=True,
                       enable_open_in_editor=True,
                       bottom_toolbar=bottom_toolbar_callback,
                       rprompt=right_prompt_callback)
        if cmd in {"q", "quit", "Quit", "exit", "Exit"}:
            break
        elif cmd in {"help", "Help", "?"}:
            show_help()
        elif cmd == "eval":
            print("42")
    except KeyboardInterrupt:
        continue
    except EOFError:
        break

12. Změna stylu zobrazení všech relevantních prvků TUI

Možnosti knihovny prompt_toolkit při tvorbě textového uživatelského rozhraní jsou samozřejmě omezenější, než je tomu u aplikací s plnohodnotným GUI. Ovšem i přesto je například možné změnit styl zobrazení jednotlivých prvků a to s využitím tabulky stylů, která se do jisté míry podobá kaskádním stylům (CSS). Podívejme se na příklad, v němž se tabulka stylů vytvoří z běžného slovníku, jehož klíči jsou názvy prvků uživatelského rozhraní a hodnotami pak jednotlivé styly, v nichž definujeme barvu popředí, barvu pozadí a popř. další vlastnosti textu (inverzní zobrazení, použití kurzívy atd.):

new_tui_style = Style.from_dict({
    'rprompt': 'bg:#ff0066 #ffffff',
    'bottom-toolbar': 'bg:#ffffff #333333 reverse',
    'prompt': 'bg:#ansiyellow #000000',
    })

Styly se pak nastaví takto:

cmd = s.prompt("Command: ",
               style=new_tui_style)

Ukázka nově ostylovaného TUI:

Obrázek 15: Pokud je příkazový řádek prázdný, zobrazuje se nulová délka a současně zpráva „???“ (neznámý příkaz).

Obrázek 16: Současné zobrazení chybového hlášení společně s dalšími prvky TUI.

Obrázek 17: Pokud uživatel napíše validní příkaz, dozví se o tom mj. i ze zprávy zobrazené na spodním toolbaru.

Styly jsou použity v dalším demonstračním příkladu, jehož úplný zdrojový kód vypadá následovně:

from prompt_toolkit.validation import Validator, ValidationError
from prompt_toolkit.styles import Style
from prompt_toolkit import PromptSession
 
 
def show_help():
    print("""Help
--------
quit - quit this application
exit - exit from this application
eval - evaluate
""")
 
 
class CommandValidator(Validator):
 
    def validate(self, document):
        user_input = document.text
 
        if user_input and not user_input.isalpha():
            index = 0
 
            for index, char in enumerate(user_input):
                if not char.isalpha():
                    break
 
            msg = "Wrong character '{c}' on index {i}".format(c=char, i=index)
            raise ValidationError(message=msg, cursor_position=index)
 
 
def get_user_input(prompt_session):
    # nejprve ziskame objekt typu Buffer
    buffer = prompt_session.default_buffer
    # z bufferu ziskame objekt typu Document
    document = buffer.document
    # ktery obsahuje atribut 'text'
    return document.text
 
 
def right_prompt_callback():
    user_input = get_user_input(s)
    return "Typed {c} characters".format(c=len(user_input))
 
 
def bottom_toolbar_callback():
    user_input = get_user_input(s)
    if user_input in {"quit", "exit", "eval", "help"}:
        return "Valid command, press Enter"
    else:
        return "???"
 
 
new_tui_style = Style.from_dict({
    'rprompt': 'bg:#ff0066 #ffffff',
    'bottom-toolbar': 'bg:#ffffff #333333 reverse',
    'prompt': 'bg:#ansiyellow #000000',
    })
 
 
s = PromptSession()
 
while True:
    try:
        cmd = s.prompt("Command: ", validator=CommandValidator(),
                       validate_while_typing=True,
                       enable_open_in_editor=True,
                       bottom_toolbar=bottom_toolbar_callback,
                       rprompt=right_prompt_callback,
                       style=new_tui_style)
        if cmd in {"q", "quit", "Quit", "exit", "Exit"}:
            break
        elif cmd in {"help", "Help", "?"}:
            show_help()
        elif cmd == "eval":
            print("42")
    except KeyboardInterrupt:
        continue
    except EOFError:
        break

13. Zvýraznění syntaxe textu zapisovaného uživatelem na vstup

Další dva demonstrační příklady budou popsány jen velmi stručně, protože se v nich používá knihovna Pygments, které bude věnován samostatný miniseriál (měl by začít vycházet již o prázdninách). První z těchto příkladů upravuje příkazový řádek takovým způsobem, aby zvýrazňoval kód podle lexikálních a syntaktických pravidel Pythonu. To zajistí nepovinný parametr lexer:

cmd = s.prompt("Command: ",
               lexer=PygmentsLexer(PythonLexer))

Obrázek 18: Zvýraznění syntaxe jazyka Python.

from pygments.lexers import PythonLexer
 
from prompt_toolkit.styles import Style
from prompt_toolkit.lexers import PygmentsLexer
from prompt_toolkit import PromptSession
 
 
def show_help():
    print("""Help
--------
quit - quit this application
exit - exit from this application
eval - evaluate
""")
 
 
new_tui_style = Style.from_dict({
    'rprompt': 'bg:#ff0066 #ffffff',
    'bottom-toolbar': 'bg:#ffffff #333333 reverse',
    'prompt': 'bg:#ansiyellow #000000',
    })
 
 
s = PromptSession()
 
while True:
    try:
        cmd = s.prompt("Command: ",
                       validate_while_typing=True,
                       enable_open_in_editor=True,
                       bottom_toolbar="Available commands: quit, exit, help, eval",
                       rprompt="Don't panic!",
                       style=new_tui_style,
                       lexer=PygmentsLexer(PythonLexer))
        if cmd in {"q", "quit", "Quit", "exit", "Exit"}:
            break
        elif cmd in {"help", "Help", "?"}:
            show_help()
        elif cmd == "eval":
            print("42")
    except KeyboardInterrupt:
        continue
    except EOFError:
        break

Ve druhém příkladu je – opět pouze pro ukázku – použita syntaxe odpovídající programovacímu jazyku Clojure:

cmd = s.prompt("Command: ",
               lexer=PygmentsLexer(ClojureLexer))

Obrázek 19: Zvýraznění syntaxe jazyka Clojure.

from pygments.lexers.jvm import ClojureLexer
 
from prompt_toolkit.styles import Style
from prompt_toolkit.lexers import PygmentsLexer
from prompt_toolkit import PromptSession
 
 
def show_help():
    print("""Help
--------
quit - quit this application
exit - exit from this application
eval - evaluate
""")
 
 
new_tui_style = Style.from_dict({
    'rprompt': 'bg:#ff0066 #ffffff',
    'bottom-toolbar': 'bg:#ffffff #333333 reverse',
    'prompt': 'bg:#ansiyellow #000000',
    })
 
 
s = PromptSession()
 
while True:
    try:
        cmd = s.prompt("Command: ",
                       validate_while_typing=True,
                       enable_open_in_editor=True,
                       bottom_toolbar="Available commands: quit, exit, help, eval",
                       rprompt="Don't panic!",
                       style=new_tui_style,
                       lexer=PygmentsLexer(ClojureLexer))
        if cmd in {"q", "quit", "Quit", "exit", "Exit"}:
            break
        elif cmd in {"help", "Help", "?"}:
            show_help()
        elif cmd == "eval":
            print("42")
    except KeyboardInterrupt:
        continue
    except EOFError:
        break

14. Vytvoření vlastního lexeru s klíčovými slovy

Pro náš testovací příkazový řádek bude samozřejmě lepší si vytvořit vlastní pravidla pro zvýraznění. Opět se jedná o téma, kterému se budeme podrobněji věnovat v samostatném seriálu. Nicméně základ jednoduchého lexeru si můžeme ukázat už nyní. Bude se jednat o třídu odvozenou od RegexLexer, což je lexer založený na rozpoznávání textu s využitím regulárních výrazů. Lexer bude rozpoznávat čtyři slova (regulární výrazy jsou tedy triviální) a pokud bude dané slovo rozpoznáno, zobrazí se stylem odpovídajícím kategorii „Keyword“ (což je standardně zelená barva). Jiný text se zobrazí stylem odpovídajícím kategorii „Generic.Error“ (což je standardně barva červená):

class CommandLexer(RegexLexer):
    name = 'command'
    aliases = ['command']
    filenames = ['*.command']
 
    tokens = {
        'root': [
            (r'quit', Keyword),
            (r'exit', Keyword),
            (r'help', Keyword),
            (r'eval', Keyword),
            (r'.+', Generic.Error),
        ]
    }

Otestování chování:

Obrázek 20: Při zadání neznámého příkazu bude text zobrazený červeně.

Obrázek 21: Známý příkaz bude zobrazen zelenou barvou.

15. Demonstrační příklad používající programátorem definovaný lexer

Celá implementace příkladu s vlastním (značně primitivním) lexerem může vypadat následovně:

from pygments.lexer import RegexLexer
from pygments.token import *
 
from prompt_toolkit.styles import Style
from prompt_toolkit.lexers import PygmentsLexer
from prompt_toolkit import PromptSession
 
 
class CommandLexer(RegexLexer):
    name = 'command'
    aliases = ['command']
    filenames = ['*.command']
 
    tokens = {
        'root': [
            (r'quit', Keyword),
            (r'exit', Keyword),
            (r'help', Keyword),
            (r'eval', Keyword),
            (r'.+', Generic.Error),
        ]
    }
 
 
def show_help():
    print("""Help
--------
quit - quit this application
exit - exit from this application
eval - evaluate
""")
 
 
new_tui_style = Style.from_dict({
    'rprompt': 'bg:#ff0066 #ffffff',
    'bottom-toolbar': 'bg:#ffffff #333333 reverse',
    'prompt': 'bg:#ansiyellow #000000',
    })
 
 
s = PromptSession()
 
while True:
    try:
        cmd = s.prompt("Command: ",
                       validate_while_typing=True,
                       enable_open_in_editor=True,
                       bottom_toolbar="Available commands: quit, exit, help, eval",
                       rprompt="Don't panic!",
                       style=new_tui_style,
                       lexer=PygmentsLexer(CommandLexer))
        if cmd in {"q", "quit", "Quit", "exit", "Exit"}:
            break
        elif cmd in {"help", "Help", "?"}:
            show_help()
        elif cmd == "eval":
            print("42")
    except KeyboardInterrupt:
        continue
    except EOFError:
        break

16. Použití myši na vstupním textovém řádku

Knihovna prompt_toolkit do jisté míry podporuje i použití myši, která nemusí sloužit pouze k operacím typu select a paste, ale lze ji využít i pro další operace, například:

  • Posun textového kurzoru na vybrané místo v rámci příkazového řádku
  • Skrolování v dlouhém řádku
  • Výběr položek z menu obsahujícím nabídku příkazů (po stisku Tab)

Použití myši v aplikaci je nutné povolit nepovinným parametrem mouse_support:

UX DAy - tip 2

s = PromptSession(completer=c)
cmd = s.prompt("Command: ", mouse_support=True)

Naposledy se dnes podívejme na demonstrační příklad, v němž je myš povolena. Pokud na příkazový řádek zadáte například pouze znak „e“, bude možné ze zobrazené nabídky vybrat příkaz „eval“ nebo „exit“. Dále bude možné měnit pozici textového kurzoru myší atd.:

from prompt_toolkit import PromptSession
from prompt_toolkit.completion import WordCompleter
 
 
def show_help():
    print("""Help
--------
quit - quit this application
exit - exit from this application
eval - evaluate
""")
 
 
c = WordCompleter(["quit", "exit", "help", "eval"], ignore_case=True)
s = PromptSession(completer=c)
 
while True:
    try:
        cmd = s.prompt("Command: ", mouse_support=True,
                       enable_open_in_editor=True,
                       bottom_toolbar="Available commands: quit, exit, help, eval",
                       rprompt="Don't panic!")
        if cmd in {"q", "quit", "Quit", "exit", "Exit"}:
            break
        elif cmd in {"help", "Help", "?"}:
            show_help()
        elif cmd == "eval":
            print("42")
    except KeyboardInterrupt:
        continue
    except EOFError:
        break

16. Repositář s demonstračními příklady

Všechny dnes popisované demonstrační příklady byly uloženy do Git repositáře dostupného na adrese https://github.com/tisnik/pre­sentations. Příklady si můžete v případě potřeby stáhnout i jednotlivě bez nutnosti klonovat celý (dnes již poměrně rozsáhlý) repositář:

# Příklad Odkaz
1 prompt10_multiline_edit.py https://github.com/tisnik/pre­sentations/blob/master/prom­pt_toolkit/prompt/prompt10_mul­tiline_edit.py
2 prompt11_ctrl_c_ctrl_d.py https://github.com/tisnik/pre­sentations/blob/master/prom­pt_toolkit/prompt/prompt11_ctrl_c_ctrl_d­.py
3 prompt12_validator_on_enter.py https://github.com/tisnik/pre­sentations/blob/master/prom­pt_toolkit/prompt/prompt12_va­lidator_on_enter.py
4 prompt13_validator_while_typing.py https://github.com/tisnik/pre­sentations/blob/master/prom­pt_toolkit/prompt/prompt13_va­lidator_while_typing.py
5 prompt14_external_editor.py https://github.com/tisnik/pre­sentations/blob/master/prom­pt_toolkit/prompt/prompt14_ex­ternal_editor.py
6 prompt15_bottom_toolbar.py https://github.com/tisnik/pre­sentations/blob/master/prom­pt_toolkit/prompt/prompt15_bot­tom_toolbar.py
7 prompt16_callback_functions.py https://github.com/tisnik/pre­sentations/blob/master/prom­pt_toolkit/prompt/prompt16_ca­llback_functions.py
8 prompt17_tui_style.py https://github.com/tisnik/pre­sentations/blob/master/prom­pt_toolkit/prompt/prompt17_tu­i_style.py
9 prompt18_input_syntax_highlight.py https://github.com/tisnik/pre­sentations/blob/master/prom­pt_toolkit/prompt/prompt18_in­put_syntax_highlight.py
10 prompt19_input_syntax_highlight_B.py https://github.com/tisnik/pre­sentations/blob/master/prom­pt_toolkit/prompt/prompt19_in­put_syntax_highlight_B.py
11 prompt20_custom_lexer.py https://github.com/tisnik/pre­sentations/blob/master/prom­pt_toolkit/prompt/prompt20_cus­tom_lexer.py
12 prompt21_mouse_support.py https://github.com/tisnik/pre­sentations/blob/master/prom­pt_toolkit/prompt/prompt21_mou­se_support.py

18. Odkazy na Internetu

  1. 4 Python libraries for building great command-line user interfaces
    https://opensource.com/article/17/5/4-practical-python-libraries
  2. prompt_toolkit 2.0.3 na PyPi
    https://pypi.org/project/prom­pt_toolkit/
  3. python-prompt-toolkit na GitHubu
    https://github.com/jonathan­slenders/python-prompt-toolkit
  4. The GNU Readline Library
    https://tiswww.case.edu/php/chet/re­adline/rltop.html
  5. GNU Readline (Wikipedia)
    https://en.wikipedia.org/wi­ki/GNU_Readline
  6. readline — GNU readline interface (Python 3.x)
    https://docs.python.org/3/li­brary/readline.html
  7. readline — GNU readline interface (Python 2.x)
    https://docs.python.org/2/li­brary/readline.html
  8. GNU Readline Library – command line editing
    https://tiswww.cwru.edu/php/chet/re­adline/readline.html
  9. gnureadline 6.3.8 na PyPi
    https://pypi.org/project/gnureadline/
  10. Editline Library (libedit)
    http://thrysoee.dk/editline/
  11. Comparing Python Command-Line Parsing Libraries – Argparse, Docopt, and Click
    https://realpython.com/comparing-python-command-line-parsing-libraries-argparse-docopt-click/
  12. libedit or editline
    http://www.cs.utah.edu/~bi­gler/code/libedit.html
  13. WinEditLine
    http://mingweditline.sourceforge.net/
  14. rlcompleter — Completion function for GNU readline
    https://docs.python.org/3/li­brary/rlcompleter.html
  15. rlwrap na GitHubu
    https://github.com/hanslub42/rlwrap
  16. rlwrap(1) – Linux man page
    https://linux.die.net/man/1/rlwrap
  17. readline(3) – Linux man page
    https://linux.die.net/man/3/readline
  18. history(3) – Linux man page
    https://linux.die.net/man/3/history
  19. vi(1) – Linux man page
    https://linux.die.net/man/1/vi
  20. emacs(1) – Linux man page
    https://linux.die.net/man/1/emacs
  21. Pygments – Python syntax highlighter
    http://pygments.org/
  22. Write your own lexer
    http://pygments.org/docs/le­xerdevelopment/
  23. TUI – Text User Interface
    https://en.wikipedia.org/wiki/Text-based_user_interface
  24. PuDB: výkonný debugger pro Python s retro uživatelským rozhraním (nástroj s plnohodnotným TUI)
    https://www.root.cz/clanky/pudb-vykonny-debugger-pro-python-s-retro-uzivatelskym-rozhranim/
  25. Historie vývoje textových editorů: krkolomná cesta k moderním textovým procesorům
    https://www.root.cz/clanky/historie-vyvoje-textovych-editoru-krkolomna-cesta-k-modernim-textovym-procesorum/

Byl pro vás článek přínosný?

Autor článku

Vystudoval VUT FIT a v současné době pracuje na projektech vytvářených v jazycích Python a Go.