Obsah
1. Textová uživatelská rozhraní naprogramovaná v jazyku Go
2. Interaktivní příkazový řádek s historií, automatickým doplňováním atd.
3. Balíček go-prompt pro aplikace naprogramované v jazyku Go
4. Příklady použití balíčku go-prompt
5. Knihovny pro ovládání terminálu i pro tvorbu plnohodnotných TUI
7. Práce s textovým terminálem s využitím knihovny tcell
9. Kostra programu, který dokáže vypsat obarvený text na terminál a reagovat na stisk kláves
10. Korektní ukončení programu
11. Vykreslení okna do plochy terminálu, reakce na změnu velikosti terminálu
12. Kreslení na plochu terminálu s využitím myši
13. Podporované styly zpráv vypisovaných na plochu terminálu
14. Standardní barvová paleta terminálů (a její nedodržování)
15. Světlý vs. tmavý text na ploše terminálu
16. Specifikace 24bitové barvy popředí (textů)
17. Specifikace 24bitové barvy pozadí
18. Změna stylu zobrazení textového kurzoru na vybraných terminálech
19. Repositář s demonstračními příklady
1. Textová uživatelská rozhraní naprogramovaná v jazyku Go
Již mnohokrát jsme si v seriálu o programovacím jazyku Go řekli, že se tento jazyk primárně používá pro tvorbu síťových utilit, mikroslužeb či dokonce ucelených webových aplikací. Je to ostatně logické, protože právě v těchto oblastech se využijí prakticky všechny užitečné vlastnosti tohoto programovacího jazyka, zejména pak podpora pro práci s gorutinami, komunikace mezi gorutinami s využitím kanálů a v neposlední řadě taktéž možnost přeložit nástroj naprogramovaný v jazyku Go do jediného binárního souboru (pro určenou platformu), který nevyžaduje prakticky žádnou instalaci (na rozdíl od aplikací v některých jiných jazycích, v nichž je nutné zajistit buď kompatibilní dynamicky linkované knihovny či dokonce celý runtime daného programovacího jazyka).
To však samozřejmě neznamená, že by se jazyk Go nemohl využívat i v dalších oblastech. Sice se (s poměrně velkou pravděpodobností) prozatím neprosadí například pro tvorbu her, ovšem naproti tomu nalezneme velké množství nástrojů naprogramovaných právě v jazyce Go, které se ovládají interaktivně příkazy zadávanými z příkazového řádku (command line) nebo které dokonce obsahují plnohodnotné textové uživatelské rozhraní (TUI). Příklady takových projektů jsou „lazy“ aplikace od Jesse Duffielda (ke kterým se někdy vrátíme) nebo fjira (prozatím tomuto textovému rozhraní pro JIRu ovšem chybí některé možnosti nabízené standardním webovým klientem):
- lazygit
https://github.com/jesseduffield/lazygit - lazydocker
https://github.com/jesseduffield/lazydocker - lazynpm
https://github.com/jesseduffield/lazynpm - fjira
https://github.com/mk-5/fjira
Obrázek 1: Textové uživatelské rozhraní lazygitu.
2. Interaktivní příkazový řádek s historií, automatickým doplňováním atd.
U poměrně velkého množství aplikací se s výhodou využije ovládání s využitím interaktivního příkazového řádku, který je doplněn o množství pomocných technik – možností editace, doplněním historie, schránky, automatickým doplňováním atd. atd. Takové aplikace tedy používají interaktivní smyčku REPL (Read-Eval-Print Loop), tj. aplikace se spustí, vypíše tzv. výzvu (prompt) uživateli, akceptuje zadané příkazy, nějakým způsobem je vykoná a opět vypíše výzvu. Zde je již většinou nutné investovat více času na přípravu prostředí aplikace, protože dnes uživatelé (po právu) vyžadují, aby nástroj s vlastní interaktivní smyčkou REPL podporoval historii příkazů, vyhledávání v historii, obarvení vstupů, podporu pro automatické doplňování příkazů atd. atd.
Obrázek 2: Velmi dobrým příkladem aplikace s interaktivní smyčkou REPL je IPython.
Pro aplikace s interaktivní smyčkou REPL programované v jazyku Go vzniklo několik knihoven, které nabízí některé či všechny výše uvedené a vyžadované funkce. Jedná se například o tyto knihovny:
- go-readline
https://github.com/fiorix/go-readline - go-prompt
https://github.com/c-bata/go-prompt - readline
https://github.com/chzyer/readline
3. Balíček go-prompt pro aplikace naprogramované v jazyku Go
Pravděpodobně nejpoužívanějším balíčkem pro Go, který zajišťuje interaktivní REPL, je balíček nazvaný go-prompt, jenž je inspirován podobným balíčkem pro Python nazvaným prompt_toolkit. S go-prompt jsme se již v tomto seriálu setkali, takže si pouze ve stručnosti uveďme jeho základní vlastnosti a způsob jeho použití. Tento balíček nabízí uživatelům následující funkce:
- Plnohodnotnou editaci na příkazovém řádku, samozřejmě včetně možnosti přesunu kurzoru s využitím příkazů Ctrl+znak, specializovaných kláves Home, End atd.
- Mazání textu před kurzorem, za kurzorem, smazání slova apod.
- Automatické doplňování příkazů na základě tabulky, kterou je možné dynamicky měnit.
- Kontextovou nápovědu s dostupnými příkazy, a to včetně popisu jednotlivých příkazů.
- Historii již zapsaných příkazů.
- Fuzzy vyhledávání příkazů.
4. Příklady použití balíčku go-prompt
Základním příkazem, který nalezneme v balíčku go-prompt, je příkaz Input, který většinu výše zmíněné funkcionality nabízí a kterému lze předat jinou funkci použitou pro doplňování příkazů a nabízení všech v dané chvíli dostupných alternativ:
package main
import "github.com/c-bata/go-prompt"
func completer(in prompt.Document) []prompt.Suggest {
return []prompt.Suggest{}
}
func main() {
login := prompt.Input("Login: ", completer)
password := prompt.Input("Password: ", completer)
println(login)
println(password)
}
Obrázek 3: Povšimněte si, že se stiskem klávesy Ctrl+A či Home můžeme bez problémů přesunout na začátek vstupního řádku. Všechny ostatní editační příkazy budou taktéž funkční.
A takto lze zajistit automatické doplňování příkazů:
package main
import (
"github.com/c-bata/go-prompt"
"os"
)
func executor(t string) {
switch t {
case "exit":
fallthrough
case "quit":
os.Exit(0)
case "help":
println("HELP:\nexit\nquit")
default:
println("Nothing happens")
}
}
func completer(in prompt.Document) []prompt.Suggest {
return []prompt.Suggest{
{Text: "help"},
{Text: "exit"},
{Text: "quit"},
}
}
func main() {
p := prompt.New(executor, completer)
p.Run()
}
Obrázek 4: Po spuštění se pouze očekává příkaz, žádná nápověda se nevypíše.
Obrázek 5: Vrácením kurzoru se zobrazí tabulka se všemi dostupnými příkazy.
Obrázek 6: Nápověda prozatím není kontextová, ovšem klávesou Tab lze příkaz doplnit.
Obrázek 7: Doplněný příkaz se zobrazí odlišnou barvou.
A nakonec si ukažme zajištění kontextové nápovědy k vybíraným příkazům:
package main
import (
"github.com/c-bata/go-prompt"
"os"
)
func executor(t string) {
switch t {
case "exit":
fallthrough
case "quit":
os.Exit(0)
case "help":
println("HELP:\nexit\nquit")
default:
println("Nothing happens")
}
return
}
func completer(in prompt.Document) []prompt.Suggest {
s := []prompt.Suggest{
{Text: "help", Description: "show help with all commands"},
{Text: "exit", Description: "quit the application"},
{Text: "quit", Description: "quit the application"},
}
return prompt.FilterHasPrefix(s, in.GetWordBeforeCursor(), true)
}
func main() {
p := prompt.New(executor, completer)
p.Run()
}
Obrázek 8: Zobrazení všech příkazů i s nápovědou.
5. Knihovny pro ovládání terminálu i pro tvorbu plnohodnotných TUI
Pro aplikace s příkazovým řádkem nebo s plnohodnotnou smyčkou REPL v naprosté většině případů plně dostačuje funkcionalita nabízená výše zmíněným balíčkem go-prompt. Ovšem v mnoha aplikacích by bylo vhodné nabídnout uživatelům plnohodnotné textové uživatelské rozhraní. Knihoven, resp. přesněji řečeno balíčků pro tvorbu textového uživatelského rozhraní v jazyku Go existuje celá řada (minimálně desítky). Jak se ovšem tyto knihovny od sebe odlišují a lze z nich vybrat tu nejlepší? Do značné míry záleží na požadavcích programátora, protože knihovny pro tvorbu textového uživatelského rozhraní je možné rozdělit do několika kategorií:
- U některých aplikací požadujeme poměrně malé množství dostupných funkcí. Typicky se jedná o možnost změny pozice textového kurzoru, změnu barvy vykreslování znaků, změnu stylu vykreslování znaků (podtržené atd.), čtení kláves bez čekání na stisk Enter a výpis znaku. Příkladem mohou být různé hry běžící v textovém režimu.
- Další skupina aplikací již vyžaduje některé sofistikovanější funkce, například možnost definice obdélníkových oken, které mohou tvořit základní abstrakci nad textovým terminálem. Ve světě jazyka C tuto vrstvu abstrakce nabízí známá knihovna ncurses a její obdobu nalezneme i v jazyce Go.
- Ve třetí skupině aplikací se nachází ty aplikace, které již potřebují vykreslit složitější a unifikované ovládací prvky (widgety), mezi něž patří tlačítka, výběrové seznamy, zatrhávací boxy, vstupní textová pole atd. Ovšem mnohé knihovny nabízí i další užitečné prvky, mezi něž patří různé typy grafů (zobrazovaných v textovém režimu) apod.
- A konečně ve čtvrté skupině nalezneme takové aplikace, které vyžadují „plnohodnotné GUI, ovšem pracující v textovém režimu“. Takové aplikace již pracují s okny (které se mohou překrývat), ovládacími prvky, reagují na události od uživatele atd. V krátkosti – moderní variantu kdysi populární knihovny Turbo Vision. I takové knihovny pro jazyk Go nalezneme, i když prozatím v podobě, která zdaleka není finální.
Obrázek 9: Ukázka možností kdysi populární knihovny TurboVision: dialog se základními informacemi o IDE Turbo Pascal 7.0.
Obrázek 10: Další ukázka použití TurboVision: textové uživatelské vývojové prostředí Borland Pascal.
Z výše uvedeného seznamu je pravděpodobně patrné, že nebude existovat jedna knihovna, která by vyhovovala všem požadavkům. Z tohoto důvodu se postupně seznámíme s několika různými knihovnami a u každé si na příkladech ukážeme její silné stránky i to, kdy je již vhodnější přejít k odlišně koncipované knihovně.
Obrázek 11: Typická aplikace s plnohodnotným menu v TUI – Midnight Commander.
6. Vybrané knihovny pro Go
Mezi knihovny určené pro programovací jazyk Go, které programátorům nabízí spíše základní funkce určené pro ovládání textového terminálu, patří termbox-go a taktéž knihovna tcell. První z těchto knihoven nalezneme na GitHubu, konkrétně na adrese https://github.com/nsf/termbox-go. Tato knihovna již sice oficiálně není udržována, ale je stále používána v některých projektech a o její popularitě svědčí i relativně vysoký počet „hvězdiček“. Naproti tomu druhá zmíněná knihovna, kterou nalezneme na stránce https://github.com/gdamore/tcell, je stále vyvíjena a dokonce pro ni existuje i komerční podpora. Ukázky použití této knihovny budou uvedeny v navazujících kapitolách.
A jaký je stav knihoven, které nabízí textové ovládací prvky či dokonce plnohodnotné textové uživatelské rozhraní? Příkladem takové knihovny může být knihovna nazvaná termui, kterou nalezneme na GitHubu na adrese https://github.com/gizak/termui. V této knihovně programátor najde relativně velké množství prvků určených pro zobrazování informací (spíše než typicky ovládacích prvků). To znamená, že se tato knihovna hodí například pro implementaci různých dashboardů. Na druhé straně spektra můžeme najít knihovnu gocui (https://github.com/jroimartin/gocui). Tato knihovna byla použita například v nástroji kcli (https://github.com/cswank/kcli), s nímž jsme se setkali v článcích o Apache Kafce.
Obrázek 12: Nápověda ke klávesovým zkratkám nástroje kcli.
Zapomenout nesmíme ani na knihovnu tview, popř. na její fork pojmenovaný cview. S možnostmi nabízenými touto knihovnou se seznámíme v samostatném článku.
7. Práce s textovým terminálem s využitím knihovny tcell
První knihovna, která programátorům umožňuje pracovat s textovým terminálem z programovacího jazyka Go, s níž se v tomto seriálu seznámíme, se jmenuje tcell. Tato knihovna umožňuje na libovolnou pozici terminálu vykreslit libovolný znak z Unicode a specifikovat u něj styl vykreslení (tučné písmo, kurzíva, podtržení, přeškrtnutí, blikání), barvu popředí, barvu pozadí a taktéž skupinu dalších znaků, které se postupně složí (představte si spojení háčku s písmenem). Taktéž je podporována změna stylu textového kurzoru, ovšem pouze za předpokladu, že tyto změny podporuje i vlastní emulátor terminálu. To pochopitelně není vše, protože terminál je vstupně-výstupním zařízením. Výstup již známe – je jím mřížka znaků. Vstupem je klávesnice a myš, popř. nějaká další operace vyvolaná například příkazem paste. V knihovně tcell se s těmito zařízeními pracuje tak, že stisk kláves, stisk tlačítek myši (ale i změna velikosti okna terminálu) generuje takzvanou událost, na kterou je možné reagovat, což uvidíme hned v další kapitole.
Obrázek 13: Ukázka aplikace naprogramované v Go, která pro vstup a výstup na terminál používá knihovnu tcell – jednoduchý manažer úloh.
8. Koncept událostí (event)
Při programování grafických uživatelských rozhraní je často používán pojem události (event(s)). Událostmi řízené programování je ostatně s programováním GUI prakticky neoddělitelně spojeno. Každý widget může v průběhu svého života generovat nějaké události. Naprostá většina událostí vzniká tak, že uživatel s widgetem interaktivně pracuje (například stlačí tlačítko zobrazené na obrazovce). Ke každému widgetu je příslušná jedna „implicitní“ událost, na kterou reaguje. Tato událost se nastavuje pomocí změny vlastnosti widgetu, což bude ukázáno v demonstračních příkladech v následujících kapitolách. Kromě implicitní události lze na widgety navázat i další události, například tlačítko (button) může reagovat i na stlačení klávesy na klávesnici, na pravé tlačítko myši či na rolování kolečkem myši.
V textových uživatelských rozhraních se s událostmi může pracovat naprosto stejným způsobem – ovšem za předpokladu, že příslušná TUI knihovna podporuje přímou manipulaci s widgety. V případě dnes popisované knihovny tcell tomu tak ovšem není – zde se pracuje na nižší úrovni. I tak je však tcell na událostech postavena, ovšem nyní jsou události spojené přímo s akcemi uživatele s terminálem a nikoli s jednotlivými ovládacími prvky. Událostí může být stisk klávesy, stisk tlačítka myši (pokud je práce s myší povolena), vložení textu ze schránky, ale například i změna velikosti okna terminálu. Na všechny tyto události může aplikace adekvátním způsobem reagovat.
Události se typicky zpracovávají v nekonečné smyčce (což není tak úplně pravda – z této smyčky se totiž vyskakuje při ukončování aplikace), v níž se postupně čte další událost z takzvané fronty událostí. Aplikace poté může na danou událost nějakým způsobem zareagovat. V kostře příkladu ukázané pod tímto odstavcem je ukázáno, jak se reaguje na dva typy událostí – změna velikosti okna terminálu a stisk nějaké klávesy. Povšimněte si, že událost nesoucí informaci o stisknuté klávese obsahuje jak kód klávesy (kurzorové šipky, Home, End, Esc atd.), tak i kód znaku v případě alfanumerického vstupu (terminál nám přitom nedokáže rozlišit například mezi Levý Shift+A, Pravý Shift+A nebo (Caps Lock) A – vždy dostaneme jen znak „A“):
for {
screen.Show()
event := screen.PollEvent()
switch ev := event.(type) {
case *tcell.EventResize:
screen.Sync()
case *tcell.EventKey:
if ev.Key() == tcell.KeyEscape || ev.Key() == tcell.KeyCtrlC {
return
} else if ev.Rune() == 'C' || ev.Rune() == 'c' {
screen.Clear()
}
}
}
9. Kostra programu, který dokáže vypsat obarvený text na terminál a reagovat na stisk kláves
Následuje program, který bude sloužit jako kostra pro všechny následující demonstrační příklady. V tomto programu, který je mj. odvozen od příkladu uvedeného v originální dokumentaci ke knihovně tcell, je definována funkce drawText určená pro výpis textu do zvoleného obdélníku (dokonce se zalomením). Interně tato funkce volá metodu Screen.SetContent určenou pro tisk jediného znaku v mřížce terminálu. Specifikovat se přitom musí souřadnice znaku, jeho kód (Unicode), případné znaky, z nichž se výsledek složí (ukážeme si příště) a styl. V hlavní funkci main je nejprve provedena inicializace obrazovky, vymazání obsahu obrazovky terminálu a vstup do smyčky událostí:
package main
import (
"log"
tcell "github.com/gdamore/tcell/v2"
)
func drawText(s tcell.Screen, x1, y1, x2, y2 int, style tcell.Style, text string) {
row := y1
column := x1
for _, r := range []rune(text) {
s.SetContent(col, row, r, nil, style)
col++
if column >= x2 {
row++
column = x1
}
if row > y2 {
break
}
}
}
func main() {
defStyle := tcell.StyleDefault.Background(tcell.ColorReset).Foreground(tcell.ColorReset)
screen, err := tcell.NewScreen()
if err != nil {
log.Fatalf("%+v", err)
}
err = screen.Init()
if err != nil {
log.Fatalf("%+v", err)
}
screen.SetStyle(defStyle)
screen.Clear()
drawText(screen, 5, 5, 20, 20, defStyle, "Hello, world!")
defer func() {
screen.Fini()
}()
for {
screen.Show()
event := screen.PollEvent()
switch ev := event.(type) {
case *tcell.EventResize:
screen.Sync()
case *tcell.EventKey:
if ev.Key() == tcell.KeyEscape || ev.Key() == tcell.KeyCtrlC {
return
} else if ev.Rune() == 'C' || ev.Rune() == 'c' {
screen.Clear()
}
}
}
}
Výsledek by měl vypadat následovně:
Obrázek 14: Takto vypadá tento demonstrační příklad po svém spuštění v emulátoru terminálu.
10. Korektní ukončení programu
V předchozím demonstračním příkladu se při jeho ukončování volala tato anonymní funkce:
defer func() {
screen.Fini()
}()
To však není zcela ideální řešení, protože nám neumožňuje korektně reagovat na všechny případné chyby v aplikaci, která by tak mohla ponechat terminál v nějakém „nepěkném“ stavu (což mnohé aplikace skutečně dělají). Korektnější řešení je opět převzato z původní dokumentace ke knihovně tcell a vypadá následovně – pokusíme se zjistit, zda došlo k nějaké chybě a pokud ano, tak se spojení s terminálem ukončí, jeho stav se obnoví a teprve poté se původní chyba znovu vyvolá:
quit := func() {
maybePanic := recover()
screen.Fini()
if maybePanic != nil {
panic(maybePanic)
}
}
defer quit()
Žádné další změny nebyly provedeny, takže výsledný zdrojový kód upraveného příkladu vypadá následovně:
package main
import (
"log"
tcell "github.com/gdamore/tcell/v2"
)
func drawText(s tcell.Screen, x1, y1, x2, y2 int, style tcell.Style, text string) {
row := y1
column := x1
for _, r := range []rune(text) {
s.SetContent(col, row, r, nil, style)
col++
if column >= x2 {
row++
column = x1
}
if row > y2 {
break
}
}
}
func main() {
defStyle := tcell.StyleDefault.Background(tcell.ColorReset).Foreground(tcell.ColorReset)
screen, err := tcell.NewScreen()
if err != nil {
log.Fatalf("%+v", err)
}
err = screen.Init()
if err != nil {
log.Fatalf("%+v", err)
}
screen.SetStyle(defStyle)
screen.Clear()
drawText(screen, 5, 5, 20, 20, defStyle, "Hello, world!")
quit := func() {
maybePanic := recover()
screen.Fini()
if maybePanic != nil {
panic(maybePanic)
}
}
defer quit()
for {
screen.Show()
event := screen.PollEvent()
switch ev := event.(type) {
case *tcell.EventResize:
screen.Sync()
case *tcell.EventKey:
if ev.Key() == tcell.KeyEscape || ev.Key() == tcell.KeyCtrlC {
return
} else if ev.Rune() == 'C' || ev.Rune() == 'c' {
screen.Clear()
}
}
}
}
Obrázek 15: Takto vypadá tento demonstrační příklad po svém spuštění v emulátoru terminálu.
11. Vykreslení okna do plochy terminálu, reakce na změnu velikosti terminálu
Poměrně velké množství aplikací rozděluje zobrazované informace do několika oken, které jsou na terminálu zobrazeny společně. Knihovna tcell sice neumožňuje plnohodnotnou práci s takovými okny (ve smyslu, že by každé takové okno bylo samostatnou strukturou s vlastními událostmi atd.), ale můžeme se alespoň pokusit o vykreslení okrajů okna – což je ostatně přesně ten způsob, který je využívaný v nástrojích lazygit, lazynpm atd. V dalším demonstračním příkladu je okno vykresleno ve funkci drawBox. Povšimněte si, že knihovna tcell obsahuje i konstanty s těmi nejpoužívanějšími znaky (runes), mezi něž patří mj. i Unicode znaky pro okraje oken:
package main
import (
"fmt"
"log"
tcell "github.com/gdamore/tcell/v2"
)
func drawText(s tcell.Screen, x1, y1, x2, y2 int, style tcell.Style, text string) {
row := y1
column := x1
for _, r := range []rune(text) {
s.SetContent(col, row, r, nil, style)
col++
if column >= x2 {
row++
column = x1
}
if row > y2 {
break
}
}
}
func drawBox(s tcell.Screen, x1, y1, x2, y2 int, style tcell.Style, text string) {
if y2 < y1 {
y1, y2 = y2, y1
}
if x2 < x1 {
x1, x2 = x2, x1
}
// Fill background
for row := y1; row <= y2; row++ {
for column := x1; column <= x2; col++ {
s.SetContent(col, row, ' ', nil, style)
}
}
// Draw borders
for column := x1; column <= x2; col++ {
s.SetContent(col, y1, tcell.RuneHLine, nil, style)
s.SetContent(col, y2, tcell.RuneHLine, nil, style)
}
for row := y1 + 1; row < y2; row++ {
s.SetContent(x1, row, tcell.RuneVLine, nil, style)
s.SetContent(x2, row, tcell.RuneVLine, nil, style)
}
// Only draw corners if necessary
if y1 != y2 && x1 != x2 {
s.SetContent(x1, y1, tcell.RuneULCorner, nil, style)
s.SetContent(x2, y1, tcell.RuneURCorner, nil, style)
s.SetContent(x1, y2, tcell.RuneLLCorner, nil, style)
s.SetContent(x2, y2, tcell.RuneLRCorner, nil, style)
}
drawText(s, x1+1, y1+1, x2-1, y2-1, style, text)
}
func drawBoxAroundScreen(screen tcell.Screen, style tcell.Style) {
const offset = 5
xmax, ymax := screen.Size()
drawBox(screen, offset, offset, xmax-offset, ymax-offset, style, fmt.Sprintf("[%d, %d]", xmax, ymax))
}
func main() {
defStyle := tcell.StyleDefault.Background(tcell.ColorBlack).Foreground(tcell.ColorReset)
boxStyle := tcell.StyleDefault.Foreground(tcell.ColorWhite).Background(tcell.ColorDarkBlue)
screen, err := tcell.NewScreen()
if err != nil {
log.Fatalf("%+v", err)
}
err = screen.Init()
if err != nil {
log.Fatalf("%+v", err)
}
screen.SetStyle(defStyle)
screen.Clear()
drawBoxAroundScreen(screen, boxStyle)
quit := func() {
maybePanic := recover()
screen.Fini()
if maybePanic != nil {
panic(maybePanic)
}
}
defer quit()
for {
screen.Show()
ev := screen.PollEvent()
switch ev := ev.(type) {
case *tcell.EventResize:
//screen.Sync()
screen.Clear()
drawBoxAroundScreen(screen, boxStyle)
case *tcell.EventKey:
if ev.Key() == tcell.KeyEscape || ev.Key() == tcell.KeyCtrlC {
return
} else if ev.Rune() == 'C' || ev.Rune() == 'c' {
screen.Clear()
}
}
}
}
Výsledek bude závislý na velikosti okna terminálu a může vypadat například následovně:
Obrázek 16: Okno zobrazené na ploše terminálu.
12. Kreslení na plochu terminálu s využitím myši
Jak jsme si již řekli v sedmé kapitole, podporuje knihovna tcell i práci s myší, což typicky znamená, že je možné zaznamenat událost typu „stisk tlačítka myši“, přičemž tato událost obsahuje i informace o pozici kurzoru myši v momentě, kdy bylo tlačítko myši stisknuto. Toho můžeme využít pro implementaci triviálního kreslicího programu, který umožňuje pokreslit plochu terminálu hvězdičkami. Postačuje nám pouze reagovat na stisk tlačítka myši (konkrétně levého tlačítka), získat souřadnice kurzoru myši a vykreslit na těchto souřadnicích hvězdičku:
case *tcell.EventMouse:
if ev.Buttons() == tcell.Button1 {
x, y := ev.Position()
drawStar(screen, x, y, starStyle)
}
Obrázek 17: Kreslení pomocí myši na ploše terminálu.
Obrázek 18: Kreslení pomocí myši na ploše terminálu.
Celý zdrojový kód tohoto demonstračního příkladu vypadá následovně:
package main
import (
"log"
tcell "github.com/gdamore/tcell/v2"
)
func drawStar(s tcell.Screen, x, y int, style tcell.Style) {
s.SetContent(x, y, '*', nil, style)
}
func main() {
defStyle := tcell.StyleDefault.Background(tcell.ColorBlack).Foreground(tcell.ColorReset)
starStyle := tcell.StyleDefault.Foreground(tcell.ColorRed).Background(tcell.ColorBlack)
screen, err := tcell.NewScreen()
if err != nil {
log.Fatalf("%+v", err)
}
err = screen.Init()
if err != nil {
log.Fatalf("%+v", err)
}
screen.EnableMouse()
screen.SetStyle(defStyle)
screen.Clear()
quit := func() {
maybePanic := recover()
screen.Fini()
if maybePanic != nil {
panic(maybePanic)
}
}
defer quit()
for {
screen.Show()
ev := screen.PollEvent()
switch ev := ev.(type) {
case *tcell.EventResize:
screen.Sync()
case *tcell.EventKey:
if ev.Key() == tcell.KeyEscape || ev.Key() == tcell.KeyCtrlC {
return
} else if ev.Rune() == 'C' || ev.Rune() == 'c' {
screen.Clear()
}
case *tcell.EventMouse:
if ev.Buttons() == tcell.Button1 {
x, y := ev.Position()
drawStar(screen, x, y, starStyle)
}
}
}
}
13. Podporované styly zpráv vypisovaných na plochu terminálu
V předchozích demonstračních příkladech jsme si ukázali, jakým způsobem se získává výchozí styl vykreslování:
style := tcell.StyleDefault
Víme také, jak tento styl upravit, například specifikací barvy popředí a/nebo barvy pozadí vykreslovaných znaků. Postup je jednoduchý – na styl aplikujeme jednu z dostupných metod, mezi něž patří i metody Foreground a Background:
defStyle := tcell.StyleDefault.Background(tcell.ColorBlack).Foreground(tcell.ColorReset) starStyle := tcell.StyleDefault.Foreground(tcell.ColorRed).Background(tcell.ColorBlack)
Kromě toho je však možné měnit i atributy vykreslovaných znaků, tedy styl písma (tučné, kurzíva), podtržení, přeškrtnutí, blikání, popř. na novějších terminálech i zobrazení textu ve formě hypertextového odkazu. Používají se k tomu následující metody, z nichž mnohé umožňují daný atribut buď povolit nebo zakázat (viz též předávaný parametr):
style.Bold(true)
style.Italic(true)
style.Underline(true)
style.StrikeThrough(true)
style.Blink(true)
style.Reverse(true)
style.Url("https://www.root.cz")
A takto může vypadat text zobrazený s využitím různých atributů:
Obrázek 19: Text zobrazený s využitím různých atributů.
Text zobrazený na devatenáctém obrázku byl získán s využitím tohoto programu:
package main
import (
"log"
tcell "github.com/gdamore/tcell/v2"
)
func drawText(s tcell.Screen, x1, y1, x2, y2 int, style tcell.Style, text string) {
row := y1
column := x1
for _, r := range []rune(text) {
s.SetContent(column, row, r, nil, style)
column++
if column >= x2 {
row++
column = x1
}
if row > y2 {
break
}
}
}
func main() {
screen, err := tcell.NewScreen()
if err != nil {
log.Fatalf("%+v", err)
}
err = screen.Init()
if err != nil {
log.Fatalf("%+v", err)
}
screen.SetStyle(tcell.StyleDefault)
screen.Clear()
style := tcell.StyleDefault
drawText(screen, 5, 1, 30, 1, style, "Normal text")
drawText(screen, 5, 2, 30, 2, style.Bold(true), "Bold text")
drawText(screen, 5, 3, 30, 3, style.Italic(true), "Italic text")
drawText(screen, 5, 4, 30, 4, style.Underline(true), "Underline text")
drawText(screen, 5, 5, 30, 5, style.StrikeThrough(true), "Strike through text")
drawText(screen, 5, 6, 30, 6, style.Blink(true), "Blink")
drawText(screen, 5, 7, 30, 7, style.Reverse(true), "Reverse")
drawText(screen, 5, 8, 30, 8, style.Url("https://www.root.cz"), "https://www.root.cz")
defer func() {
screen.Fini()
}()
for {
screen.Show()
event := screen.PollEvent()
switch ev := event.(type) {
case *tcell.EventResize:
screen.Sync()
case *tcell.EventKey:
if ev.Key() == tcell.KeyEscape || ev.Key() == tcell.KeyCtrlC {
return
} else if ev.Rune() == 'C' || ev.Rune() == 'c' {
screen.Clear()
}
}
}
}
14. Standardní barvová paleta terminálů (a její nedodržování)
Barvu popředí (znaků) či pozadí lze vybírat ze standardní palety, která obsahuje šestnáct barev s indexy 0 až 15, které jsou doplněny o několik odstínů šedi. Mnohé terminály ovšem mají barvovou paletu větší, například se setkáme s 88 barvami v paletě (xterm), 256 barvami atd. (a několik terminálů naopak podporuje jen osm barev). Barvu znaku můžeme nastavit následovně (za i se volí index příslušné barvy):
style.Foreground(tcell.PaletteColor(i))
Problém (a současně i výhoda) spočívá v tom, že barvovou paletu je možné měnit v konfiguraci konkrétního terminálu. Tím lze dosáhnout toho, že například původní červená barva se ve skutečnosti zobrazí jako světle modrá atd. – vše podle požadavků uživatele. Na druhou stranu ovšem nebudou všechny aplikace vypadat tak, jak to plánoval jejich tvůrce. Ostatně se podívejme sami:
Obrázek 20: Standardních 16 barev zobrazených v jednom typu terminálu.
Obrázek 21: Standardních 16 barev zobrazených v jiném typu terminálu.
Předchozí dva obsahy terminálu byly vykresleny tímto demonstračním příkladem:
package main
import (
"fmt"
"log"
tcell "github.com/gdamore/tcell/v2"
)
func drawText(s tcell.Screen, x1, y1, x2, y2 int, style tcell.Style, text string) {
row := y1
column := x1
for _, r := range []rune(text) {
s.SetContent(column, row, r, nil, style)
column++
if column >= x2 {
row++
column = x1
}
if row > y2 {
break
}
}
}
func main() {
screen, err := tcell.NewScreen()
if err != nil {
log.Fatalf("%+v", err)
}
err = screen.Init()
if err != nil {
log.Fatalf("%+v", err)
}
screen.SetStyle(tcell.StyleDefault)
screen.Clear()
style := tcell.StyleDefault
for i := 0; i < 16; i++ {
s := style.Foreground(tcell.PaletteColor(i))
msg := fmt.Sprintf("Color #%d", i)
drawText(screen, 5, i, 30, i, s, msg)
}
defer func() {
screen.Fini()
}()
for {
screen.Show()
event := screen.PollEvent()
switch ev := event.(type) {
case *tcell.EventResize:
screen.Sync()
case *tcell.EventKey:
if ev.Key() == tcell.KeyEscape || ev.Key() == tcell.KeyCtrlC {
return
} else if ev.Rune() == 'C' || ev.Rune() == 'c' {
screen.Clear()
}
}
}
}
15. Světlý vs. tmavý text na ploše terminálu
S barvami do jisté míry souvisí i poslední textový atribut, který jsme si zatím nepopsali. Tento atribut se jmenuje dim a umožňuje snížit intenzitu barvy textu. Pokud namísto volání:
s := style.Background(tcell.PaletteColor(i))
použijeme toto volání:
s := style.Background(tcell.PaletteColor(i)).Dim(true)
bude na většině terminálů text zobrazen se sníženou intenzitou.
Podívejme se na získané výsledky, opět zobrazené na dvou různých emulátorech terminálu, z nichž je patrné, jak se od sebe mohou reálně zobrazené barvy odlišovat:
Obrázek 22: Standardních 16 barev pozadí zobrazených v jednom typu terminálu.
Obrázek 23: Standardních 16 barev pozadí zobrazených v jiném typu terminálu.
Vše si pochopitelně můžeme vyzkoušet i prakticky:
package main
import (
"fmt"
"log"
tcell "github.com/gdamore/tcell/v2"
)
func drawText(s tcell.Screen, x1, y1, x2, y2 int, style tcell.Style, text string) {
row := y1
column := x1
for _, r := range []rune(text) {
s.SetContent(column, row, r, nil, style)
column++
if column >= x2 {
row++
column = x1
}
if row > y2 {
break
}
}
}
func main() {
screen, err := tcell.NewScreen()
if err != nil {
log.Fatalf("%+v", err)
}
err = screen.Init()
if err != nil {
log.Fatalf("%+v", err)
}
screen.SetStyle(tcell.StyleDefault)
screen.Clear()
style := tcell.StyleDefault
for i := 0; i < 16; i++ {
s := style.Background(tcell.PaletteColor(i))
msg := fmt.Sprintf("Color #%d", i)
drawText(screen, 5, i, 30, i, s, msg)
}
for i := 0; i < 16; i++ {
s := style.Background(tcell.PaletteColor(i)).Dim(true)
msg := fmt.Sprintf("Color #%d", i)
drawText(screen, 55, i, 75, i, s, msg)
}
defer func() {
screen.Fini()
}()
for {
screen.Show()
event := screen.PollEvent()
switch ev := event.(type) {
case *tcell.EventResize:
screen.Sync()
case *tcell.EventKey:
if ev.Key() == tcell.KeyEscape || ev.Key() == tcell.KeyCtrlC {
return
} else if ev.Rune() == 'C' || ev.Rune() == 'c' {
screen.Clear()
}
}
}
}
16. Specifikace 24bitové barvy popředí (textů)
V předchozích kapitolách jsme si řekli, že textové terminály většinou podporují osm, šestnáct, 88, 256, popř. 224 barev (xterm pak 4096 barev, ovšem ve výchozím nastavení jen 88 barev). Knihovna tcell dokáže pracovat s celým barvovým spektrem, tedy s oněmi více než šestnácti miliony barvových odstínů. Záleží pak na konkrétním textovém terminálu, zda dokáže všechny barvy zobrazit nebo například provede převod na nejbližší barvu z palety 4096 odstínů. Pro převod vlastní barvy (reprezentované v barvovém prostoru RGB) na příslušný řídicí kód (escape sekvenci) slouží funkce nazvaná NewRGBColor. Tato funkce je použita v dalším demonstračním příkladu, v němž se pokusíme zobrazit text s proměnlivou barvou popředí:
Obrázek 24: Část výstupu produkovaného demonstračním příkladem z této kapitoly.
Úplný zdrojový kód demonstračního příkladu, který vykreslil text tak, jak je patrný na obrázku číslo 24, vypadá takto:
package main
import (
"fmt"
"log"
tcell "github.com/gdamore/tcell/v2"
)
func drawText(s tcell.Screen, x1, y1, x2, y2 int, style tcell.Style, text string) {
row := y1
column := x1
for _, r := range []rune(text) {
s.SetContent(column, row, r, nil, style)
column++
if column >= x2 {
row++
column = x1
}
if row > y2 {
break
}
}
}
func drawPalette(screen tcell.Screen, green int32, offset int) {
style := tcell.StyleDefault
for j := 0; j < 16; j++ {
for i := 0; i < 16; i++ {
color := tcell.NewRGBColor(int32(i<<4), green, int32(j<<4))
s := style.Foreground(tcell.Color(color))
msg := fmt.Sprintf(" %02x%02x%02x ", i, green, j)
drawText(screen, i*10, j+offset, i*10+8, j+offset, s, msg)
}
}
}
func main() {
screen, err := tcell.NewScreen()
if err != nil {
log.Fatalf("%+v", err)
}
err = screen.Init()
if err != nil {
log.Fatalf("%+v", err)
}
screen.SetStyle(tcell.StyleDefault)
screen.Clear()
defer func() {
screen.Fini()
}()
drawPalette(screen, 0, 1)
drawPalette(screen, 255, 20)
for {
screen.Show()
event := screen.PollEvent()
switch ev := event.(type) {
case *tcell.EventResize:
screen.Sync()
case *tcell.EventKey:
if ev.Key() == tcell.KeyEscape || ev.Key() == tcell.KeyCtrlC {
return
} else if ev.Rune() == 'C' || ev.Rune() == 'c' {
screen.Clear()
}
}
}
}
17. Specifikace 24bitové barvy pozadí
Z celého barvového spektra s šestnácti miliony barvových odstínů je možné vybrat i barvu pozadí (background) znaků, což je ukázáno v dalším, dnes již předposledním, demonstračním příkladu (obecně platí, že terminály, které dokážou zobrazit 24bitové barvy popředí, podporují i 24bitové barvy pozadí):
Obrázek 25: Část výstupu produkovaného demonstračním příkladem z této kapitoly.
Opět si ukažme celý zdrojový kód příkladu, který vyprodukuje tento výstup:
package main
import (
"fmt"
"log"
tcell "github.com/gdamore/tcell/v2"
)
func drawText(s tcell.Screen, x1, y1, x2, y2 int, style tcell.Style, text string) {
row := y1
column := x1
for _, r := range []rune(text) {
s.SetContent(column, row, r, nil, style)
column++
if column >= x2 {
row++
column = x1
}
if row > y2 {
break
}
}
}
func drawPalette(screen tcell.Screen, green int32, offset int) {
style := tcell.StyleDefault
for j := 0; j < 16; j++ {
for i := 0; i < 16; i++ {
color := tcell.NewRGBColor(int32(i<<4), green, int32(j<<4))
s := style.Background(tcell.Color(color))
msg := fmt.Sprintf(" %02x%02x%02x ", i, green, j)
drawText(screen, i*10, j+offset, i*10+8, j+offset, s, msg)
}
}
}
func main() {
screen, err := tcell.NewScreen()
if err != nil {
log.Fatalf("%+v", err)
}
err = screen.Init()
if err != nil {
log.Fatalf("%+v", err)
}
screen.SetStyle(tcell.StyleDefault)
screen.Clear()
defer func() {
screen.Fini()
}()
drawPalette(screen, 0, 1)
drawPalette(screen, 255, 20)
for {
screen.Show()
event := screen.PollEvent()
switch ev := event.(type) {
case *tcell.EventResize:
screen.Sync()
case *tcell.EventKey:
if ev.Key() == tcell.KeyEscape || ev.Key() == tcell.KeyCtrlC {
return
} else if ev.Rune() == 'C' || ev.Rune() == 'c' {
screen.Clear()
}
}
}
}
18. Změna stylu zobrazení textového kurzoru na vybraných terminálech
Poslední vlastností, která je ovšem podporována pouze některými textovými terminály, je schopnost programově změnit tvar textového kurzoru. K dispozici je několik možných tvarů a vlastností textového kurzoru, ovšem – podobně jako v případě barev – i zde platí, že do značné míry záleží na konfiguraci konkrétního terminálu a v neposlední řadě i na jeho reálných schopnostech (textový terminál na PC například nedokáže zobrazit kurzor ve tvaru vertikálního I, protože to neumožňuje jeho hardware). Samostatnou kapitolou je pak automatická změna tvaru kurzoru ve chvíli, kdy terminál ztratí fokus. Nicméně i přes tyto potenciální nedostatky je možné si vybrat z následujících tvarů kurzoru:
- Blokový kurzor
- Blikající blokový kurzor
- Kurzor ve tvaru horizontálního podtržení (_)
- Blikající kurzor ve tvaru horizontálního podtržení (_)
- Kurzor ve tvaru I
- Blikající kurzor ve tvaru I
Výsledky mohou vypadat následovně (v příkladu se kurzory přepínají klávesami 1 až 6):
Obrázek 26: Blokový kurzor (xfce4-terminal).
Obrázek 27: Horizontální kurzor (xfce4-terminal).
Obrázek 28: Vertikální kurzor (xfce4-terminal).
Obrázek 29: Blokový kurzor (xterm).
Obrázek 30: Horizontální kurzor (xterm).
Obrázek 31: Vertikální kurzor (xterm).
A pochopitelně si na závěr ukažme úplný zdrojový kód dnešního posledního demonstračního příkladu:
package main
import (
"log"
tcell "github.com/gdamore/tcell/v2"
)
func main() {
screen, err := tcell.NewScreen()
if err != nil {
log.Fatalf("%+v", err)
}
err = screen.Init()
if err != nil {
log.Fatalf("%+v", err)
}
screen.SetStyle(tcell.StyleDefault)
screen.Clear()
screen.ShowCursor(10, 5)
defer func() {
screen.Fini()
}()
for {
screen.Show()
event := screen.PollEvent()
switch ev := event.(type) {
case *tcell.EventResize:
screen.Sync()
case *tcell.EventKey:
switch ev.Key() {
case tcell.KeyEscape:
fallthrough
case tcell.KeyCtrlC:
return
}
switch ev.Rune() {
case 'C':
fallthrough
case 'c':
screen.Clear()
case '1':
screen.SetCursorStyle(tcell.CursorStyleBlinkingBlock)
case '2':
screen.SetCursorStyle(tcell.CursorStyleSteadyBlock)
case '3':
screen.SetCursorStyle(tcell.CursorStyleBlinkingUnderline)
case '4':
screen.SetCursorStyle(tcell.CursorStyleSteadyUnderline)
case '5':
screen.SetCursorStyle(tcell.CursorStyleBlinkingBar)
case '6':
screen.SetCursorStyle(tcell.CursorStyleSteadyBar)
}
}
}
}
19. Repositář s demonstračními příklady
Zdrojové kódy všech dnes použitých demonstračních příkladů naprogramovaných v jazyku Go byly uloženy do Git repositáře, který je dostupný na adrese https://github.com/tisnik/go-root. V případě, že nebudete chtít klonovat celý repositář, můžete namísto toho použít odkazy na jednotlivé demonstrační příklady, které naleznete v následující tabulce:
20. Odkazy na Internetu
- Tvorba aplikací a her s textovým uživatelským rozhraním s využitím knihovny Blessed
https://www.root.cz/clanky/tvorba-aplikaci-a-her-s-textovym-uzivatelskym-rozhranim-s-vyuzitim-knihovny-blessed/ - Tvorba aplikací a her s textovým rozhraním s knihovnou Blessed (dokončení)
https://www.root.cz/clanky/tvorba-aplikaci-a-her-s-textovym-rozhranim-s-knihovnou-blessed-dokonceni/ - Text-Based User Interfaces
https://appliedgo.net/tui/ - PTerm: A powerful TUI framework written in Go
https://pterm.sh/ - termbox-go
https://github.com/nsf/termbox-go - tcell
https://github.com/gdamore/tcell - termui
https://github.com/gizak/termui - GOCUI – Go Console User Interface
https://github.com/jroimartin/gocui - Fork předešlého
https://github.com/jesseduffield/gocui - CLUI
https://github.com/VladimirMarkelov/clui - tview – Rich Interactive Widgets for Terminal UIs
https://github.com/rivo/tview - cview
https://code.rocket9labs.com/tslocum/cview - ANSI Escape Code – Colors
https://en.wikipedia.org/wiki/ANSI_escape_code#Colors - A curated list of awesome Go frameworks, libraries and software
https://awesome-go.com/ - Aurora
https://github.com/logrusorgru/aurora - colourize
https://github.com/TreyBastian/colourize - go-colortext
https://github.com/daviddengcn/go-colortext - blessed na PyPi
https://pypi.org/project/blessed/ - blessed na GitHubu
https://github.com/jquast/blessed - Blessed documentation!
https://blessed.readthedocs.io/en/latest/ - termbox-go na GitHubu
https://github.com/nsf/termbox-go - termui na GitHubu
https://github.com/gizak/termui - blessed na GitHubu
https://github.com/chjj/blessed - blessed-contrib na GitHubu
https://github.com/yaronn/blessed-contrib - tui-rs na GitHubu
https://github.com/fdehau/tui-rs - asciigraph
https://github.com/guptarohit/asciigraph - Standardní balíček text/tabwriter
https://golang.org/pkg/text/tabwriter/ - Elastic tabstops: A better way to indent and align code
https://nickgravgaard.com/elastic-tabstops/ - ASCII Table Writer
https://github.com/olekukonko/tablewriter - TablePrinter
https://github.com/lensesio/tableprinter - go-pretty
https://github.com/jedib0t/go-pretty - cfmt
https://github.com/mingrammer/cfmt - box-cli-maker
https://github.com/Delta456/box-cli-maker - go-prompt
https://github.com/c-bata/go-prompt - lazygit
https://github.com/jesseduffield/lazygit - lazydocker
https://github.com/jesseduffield/lazydocker - lazynpm
https://github.com/jesseduffield/lazynpm - Awesome TUIs – list of projects that provide terminal user interfaces
https://github.com/rothgar/awesome-tuis - fjira
https://github.com/mk-5/fjira