SlideShare une entreprise Scribd logo
TwinUI
Une interface texte plein écran
et une interface Web
dans une même application
https://osinet.fr/go
Frédéric G. MARAND
Ingénieur conseil
en performance logicielle
32
TABLE OF CONTENTS
Le besoin
Pourquoi 2 interfaces ?
Choix techniques
Quels modules ? Pourquoi ?
TView
Comment TView est-il conçu?
1
54
Code
Comment coder son UI texte
?
Combinaison
Comment assembler les 2 UIs ?
Le besoin
Le besoin: pourquoi 2 interfaces ?
● L’interface Web est la référence pour
les consommateurs
● La ligne de commande est le
quotidien des développeurs
Le besoin: pourquoi 2 interfaces ?
● Lancer un programme graphique (IDE…)
● Chercher les options d’un programme
● Assembler un pipeline pour un script
● Utiliser vim ou emacs, atop/htop ...
● Administrer une instance en SSH
● Contrôler un programme en cours (docker)
Le besoin: pourquoi 2 interfaces ?
● Exemple: “un livre dont vous êtes le
héros” aka “choose your own adventure”
● L’original: un projet de Ion Calhoun sur
son site Gophercises
Le besoin: pourquoi 2 interfaces ?
https://gophercises.com/
Le besoin: pourquoi 2 interfaces ?
Choix
techniques
Choix techniques: côté Web - pour cette démo
● Pourquoi un framework web plutôt qu’un autre ?
● Aucun: bien pour un micro-service, rarement au-delà
○ Chemins dynamiques
○ Chargeurs / validateurs d’arguments
○ Filtres par méthode
○ Montages de sous-groupes
● Gorilla/mux:
○ minimal
○ popularité maximale
○ modèle des middlewares
○ utilisé pour l’exemple, facile à convertir
Choix techniques: côté Web - pour un projet lourd
● Gin https://gin-gonic.com/
○ plutôt orienté APIs
○ rapide, simple
● Beego https://beego.me/
○ inspiration Django,
○ simple, bien outillé (ORM, ligne de commande)
● Buffalo https://gobuffalo.io/fr/
○ pour les projets Web classiques
○ riche: adoption de composants tiers
○ ORM Pop, moteur de templates Plush, I18n…
○ documentation en français
● Revel http://revel.github.io/
○ Positionnement Buffalo mais depuis 2013
Choix techniques: côté texte - l’offre
● Préhistoire: https://github.com/gbin/goncurses
● nsf/termbox-go : ncurses repensé en Go. Très populaire. Abandonné.
● gdamore/tcell : le successeur. Vitesse, Unicode, couleurs, souris.
● gizak/termui : graphique pour tableaux de bord comme expvarmon
● JoelOtter/termloop : spécialisé pour les jeux plein écran texte
● jroimartin/gocui : gestion de “fenêtres” texte basique
● rivo/tview : widgets, fenêtres, application, layout (flex!), 256 couleurs,
souris, son, Unicode, basé sur tcell.
● VladimirMarkelov/clui: TurboVision TNG :-O
tview FTW !
TView
TView: dépendances
TView: démo
TView: organisation du code
● Créer des Widgets: Button, Text, List …
● Les configurer
○ Configurables par leur champ promu Box: SetTitle, SetBackgroundColor ...
○ Et aussi avec leurs propres méthodes: Checkbox.SetChecked, Table.InsertRow …
● Leur ajouter des handlers d’événement s’ils composent FormItem
○ Soit à l’ajout d’enfants: List.AddItem(pri, sec, run, func()) *List
○ Soit directement: List.SetSelectedFunc(func(int, string, string, rune) *List)
● Créer une application avec NewApplication
● Définir quel widget est la racine de l’arborescence avec SetRoot
● Lui ajouter un handler d’événement par SetInputCapture
● Lancer la boucle événementielle avec la méthode Run.
TView: composants
package main
import "github.com/rivo/tview"
func main() {
tv := tview.NewButton("Hello, world!")
tview.NewApplication().SetRoot(tv,
true).Run()
}
TView: les composants
TView: composants personnalisés
● Créer ses composants = implémenter TView.Primitive
○ Blur, Draw, Focus, GetFocusable, GetRect, InputHandler, MouseHandler, SetRect
○ Soit directement, soit en composant un Box comme font les autres
● Plus simplement:
○ Surcharger le Draw d’un composant approprié
○ Intercepter les rafraîchissements par les hooks Application.Draw:
SetBeforeDrawFunc(), SetAfterDrawFunc()
○ Intercepter les événements
■ Globalement: Application.SetInputCapture()
■ Sur une Primitive: Box.SetInputCapture()
Coder ses
deux UIs
Coder ses UI: web
Layout:
● arc.gohtml
● gopher.json
● main.go
● README.md
● story.go
● style.css
● web.go
Un même package main:
● Template
● Données
● Point d’entrée de l’application
● Documentation
● Modèle de données
● Styles
● Handlers web
Coder ses UI: web - main.go
package main
import (
"flag"
"html/template"
"log"
"net/http"
"os"
"strconv"
"github.com/gorilla/mux"
)
func main() {
path := flag.String("story", "gopher.json",
"The name of the file containing the data")
port := flag.Int("port", 8080, "TCP port")
flag.Parse()
story, err := loadStory(*path)
if err != nil {
log.Fatalln(err)
}
//...suite...
tpl, err := template.ParseFiles("arc.gohtml")
if err != nil {
log.Println("Failed parsing arc template", err)
os.Exit(2)
}
r := mux.NewRouter()
r.HandleFunc("/style.css", styleHandler)
r.HandleFunc("/arc/{arc}",
func(w http.ResponseWriter, r *http.Request) {
arcHandler(w, r, story, tpl)
})
r.HandleFunc("/",
func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/arc/intro",
http.StatusMovedPermanently)
})
_ = http.ListenAndServe(":"+strconv.Itoa(*port), r)
}
Coder ses UI: web - arc.gohtml
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Choose Your Own Adventure: {{.Title}}</title>
<link rel="stylesheet" href="/style.css"/>
</head>
<body>
<section class="page">
<h1>{{ .Title }}</h1>
{{ range .Story }}
<p>{{ . }}</p>
{{ end }}
{{ if .Options }}
<ul>
{{ range .Options }}
<li><a href="/arc/{{ .ArcName }}">
{{ .Text }}</a></li>
{{ end }}
</ul>
{{ end }}
</section>
<footer>
This is a demo of an exercise from the
free course
<a href="https://gophercises.com">
Gophercises</a>. Check it out
if you are interested in learning /
practicing Go.
</footer>
</body>
</html>
Coder ses UI: web - story.go
package main
import (
"encoding/json"
"os"
)
// Option represents a choice at the end of an arc.
type Option struct {
Text string
ArcName string `json:"arc"`
}
// Arc represents an individual narrative structure.
type Arc struct {
Title string
Story []string
Options []Option
}
// Story is the graph of arcs.
type Story map[string]Arc
func loadStory(path string) (Story, error) {
story := Story{}
file, err := os.Open(path)
if err != nil {
return story, err
}
decoder := json.NewDecoder(file)
err = decoder.Decode(&story)
return story, err
}
Coder ses UI: web - web.go
func arcHandler(w http.ResponseWriter,
r *http.Request, story Story,
tpl *template.Template) {
name, ok := mux.Vars(r)["arc"]
if !ok {
log.Println("arcHandler called without an arc")
http.Redirect(w, r, "/",
http.StatusMovedPermanently)
return
}
arc, ok := story[name]
if !ok {
log.Printf("Incorrect arc requested: %sn", name)
http.NotFound(w, r)
return
}
err := tpl.Execute(w, arc)
if err != nil {
log.Printf("Arc template %#v: %vn", arc, err)
w.WriteHeader(http.StatusInternalServerError)
return
}
}
func styleHandler(w http.ResponseWriter,
r *http.Request) {
file, err := os.Open("style.css")
if err != nil {
log.Println(err)
http.NotFound(w, r)
return
}
defer file.Close()
w.Header().Set("Content-Type", "text/css")
_, err = io.Copy(w, file)
if err != nil {
log.Println("Failed sending CSS", err)
w.WriteHeader(
http.StatusInternalServerError)
}
}
Coder ses UI: web - rendu
Coder ses UI: terminal - primitives
Coder ses UI: terminal - layout
Layout:
● ../gophercises_cyoa/gopher.json
● main.go
● model.go
● ui.go
Un même package main:
● Données
● Point d’entrée de l’application
● Accès aux données
● Composants d’interface
Coder ses UI: terminal - main.go
func initUI(story *Story)
(*tview.Application, *View) {
view := NewView(story)
app := tview.NewApplication()
app.SetRoot(view.Grid, true).
EnableMouse(true).
SetInputCapture(func(event *tcell.EventKey)
*tcell.EventKey {
switch event.Key() {
case tcell.KeyEsc:
app.Stop()
case tcell.KeyRune:
u := view.URLFromKey(event)
switch {
case u == `quit`:
app.Stop()
case u != ``:
view.Handle(u)
}
}
return nil
})
return app, view
}
func initModel(path string) (*Story, error) {
story := make(Story)
err := story.Load(path)
if err != nil {
return nil, fmt.Errorf(`loading story: %w`, err)
}
return &story, nil
}
func main() {
path := flag.String(`story`, `./gophercises_cyoa/gopher.json`,
` "The name of the file containing the data`)
flag.Parse()
story, err := initModel(*path)
if err != nil {
log.Fatalf("Starting model: %vn", err)
}
app, view := initUI(story)
view.Handle(`intro`)
if err := app.Run(); err != nil {
log.Fatalf("Running app: %vn", err)
}
}
Coder ses UI: terminal - model.go
package main
import (
"encoding/json"
"fmt"
"os"
)
// Option represents a choice at the end of an arc.
type Option struct {
Label string `json:"text"`
URL string `json:"arc"`
}
// Arc represents an individual narrative structure.
type Arc struct {
Title string `json:"title"`
Body []string `json:"story"`
Options []Option `json:"options"`
}
// Story is the graph of arcs.
type Story map[string]Arc
// Load fetches the story data from disk.
func (s *Story) Load(path string) error {
file, err := os.Open(path)
if err != nil {
return fmt.Errorf(`opening %s: %w`, path,
err)
}
decoder := json.NewDecoder(file)
err = decoder.Decode(s)
return err
}
// Arc obtains an arc from its URL.
func (s *Story) Arc(url string) *Arc {
a, ok := (*s)[url]
if !ok {
return nil
}
return &a
}
Coder ses UI: terminal - ui.go
package main
import (
"fmt"
"log"
"github.com/gdamore/tcell"
"github.com/rivo/tview"
)
// View holds the structure of the application View:
type View struct {
// Heading is the top line.
Heading *tview.TextView
// Body is the main frame.
Body *tview.TextView
// Actions contains the action menu.
Actions *tview.List
// Grid is the container wrapping Heading, Body, Actions.
*tview.Grid
// Story is the model from which the View reads data.
*Story
}
func (v View) Handle(url string) {
arc := v.Story.Arc(url)
if arc == nil {
log.Printf("Path not found: %sn", url)
return
}
fmt.Fprint(v.Heading.Clear(), arc.Title)
b := v.Body.Clear()
for _, row := range arc.Body {
fmt.Fprintln(b, row + "n")
}
v.Actions.Clear()
if len(arc.Options) == 0 {
arc.Options = []Option{{
Label: `Leave story`,
URL: `quit`,
}}
}
for k, item := range arc.Options {
v.Actions.InsertItem(k, item.Label,
item.URL, rune('a' + k), nil)
}
}
Coder ses UI: terminal - ui.go (2)
func textView(title string) *tview.TextView {
tv := tview.NewTextView().
SetTextAlign(tview.AlignLeft).
SetTextColor(tcell.ColorBlack)
tv.SetBackgroundColor(tcell.ColorWhite).
SetBorderColor(tcell.ColorLightGray).
SetBorder(true)
tv.SetTitle(` ` + title + ` `).
SetTitleColor(tcell.ColorSlateGray).
SetTitleAlign(tview.AlignLeft)
return tv
}
func list(title string) *tview.List {
l := tview.NewList().
SetMainTextColor(tcell.ColorBlack).
ShowSecondaryText(false).
SetShortcutColor(tcell.ColorDarkGreen)
l.SetBackgroundColor(tcell.ColorWhite).
SetBorderColor(tcell.ColorLightGray).
SetBorder(true).
SetTitle(` ` + title + ` `).
SetTitleColor(tcell.ColorSlateGray).
SetTitleAlign(tview.AlignLeft)
return l
}
// NewView builds an initialized View.
func NewView(story *Story) *View {
v := &View{
Heading: textView("Scene"),
Body: textView("Description").SetScrollable(true),
Actions: list("Choose wisely"),
Grid: tview.NewGrid(),
Story: story,
}
v.Grid.
SetRows(3, 0, 5). // 1-row title, 3-row actions. Add 2
for their own borders.
SetBorders(false). // Use the view borders instead.
AddItem(v.Heading, 0, 0, 1, 1, 0, 0, false).
AddItem(v.Body, 1, 0, 1, 1, 0, 0, false).
AddItem(v.Actions, 2, 0, 1, 1, 0, 0, true)
return v
}
Coder ses UI: web - résultat
Composer
TwinUI
TwinUI: composer les 2 UIs
● 2 paquets main, chacun dans leur répertoire
● Problème: chacun a sa logique d’accès aux données
● Problème: chacun se termine par une fonction bloquante:
○ Web: _ = http.ListenAndServe(":"+strconv.Itoa(*port), r)
○ TView: _ = app.Run()
● Refactoring !
TwinUI: composer les 2 UIs
Layout:
● main.go
● model/
○ gopher.json
○ model.go
● tview/
○ ui.go
● web/
○ arc.gohtml
○ style.css
○ web.go
4 paquets (...mais…)
● Point d’entrée de l’application
● Source de données
○ Données
○ Accès aux données
● Interacteur: terminal
○ Composants texte
● Interacteur: web
○ Template
○ Styles
○ Composants web
TwinUI: composer les 2 UIs
Si ça vous rappelle quelque
chose, ce n’est pas un
hasard…
À l’échelle d’un projet réel, la
généralisation est souvent
représentée comme ceci et
appelée “architecture
hexagonale”.
Image: Blog Netflix
https://netflixtechblog.com/ready-for-
changes-with-hexagonal-architecture-
b315ec967749
TwinUI: le secret est dans le main
func main() {
path := flag.String(`story`, `./model/gopher.json`, `The name of the data file`)
port := flag.Int("port", 8080, "The TCP port on which to listen")
flag.Parse()
// Initialize model: without data, we can't proceed.
story, err := initModel(*path)
if err != nil { log.Fatalf("Starting model: %vn", err) }
defer story.Close()
// Initialize the twin UIs.
app := initTextUI(story)
router := initWebUI(story)
....
TwinUI: le secret est dans le main
….
// Run the twin UIs, exiting the app whenever either of them exits.
done := make(chan bool)
go func() {
if err := app.Run(); err != nil {
log.Fatalf("Running text app: %vn", err)
}
done <- true
}()
go func() {
if err := http.ListenAndServe(":"+strconv.Itoa(*port), router); err !=
nil {
log.Fatalf("Running web app: %vn", err)
}
done <- true
}()
<-done
TwinUI: one last thing...
● Et les erreurs ? log utilise la sortie d’erreur, donc le terminal
○ Mais nous avons déjà une UI plein écran en cours
TwinUI: one last thing...
● tview.TermView implémente io.Writer:
● log.SetOutput(&logger{app: app, Writer: view.Body})
https://github.com/fgm/twinui
?QUESTIONS
malt-academy.com

Contenu connexe

Tendances

TypeScript for dummies
TypeScript for dummiesTypeScript for dummies
TypeScript for dummies
Microsoft
 
Présentation JavaScript
Présentation JavaScriptPrésentation JavaScript
Présentation JavaScript
tarkan_
 
C3 - Langage C - ISIMA 1 - Troisieme partie
C3 - Langage C - ISIMA 1 - Troisieme partieC3 - Langage C - ISIMA 1 - Troisieme partie
C3 - Langage C - ISIMA 1 - Troisieme partie
Loic Yon
 
Présentation de ECMAScript 6
Présentation de ECMAScript 6Présentation de ECMAScript 6
Présentation de ECMAScript 6
Julien CROUZET
 
L'Open Web en tant que pierre angulaire du développement multi-objets
L'Open Web en tant que pierre angulaire du développement multi-objetsL'Open Web en tant que pierre angulaire du développement multi-objets
L'Open Web en tant que pierre angulaire du développement multi-objets
Thomas Bassetto
 
Cours javascript v1
Cours javascript v1Cours javascript v1
Cours javascript v1
TheBest Icanbe
 
Découverte d'UNIX - ISIMA
Découverte d'UNIX - ISIMADécouverte d'UNIX - ISIMA
Découverte d'UNIX - ISIMA
Loic Yon
 
Introduction TypeScript
Introduction TypeScriptIntroduction TypeScript
Introduction TypeScript
felixbillon
 
Introduction à JavaScript
Introduction à JavaScriptIntroduction à JavaScript
Introduction à JavaScript
Microsoft
 
Javascript un langage supérieur
Javascript un langage supérieurJavascript un langage supérieur
Javascript un langage supérieur
Fredy Fadel
 
Notions de base de JavaScript
Notions de base de JavaScriptNotions de base de JavaScript
Notions de base de JavaScript
Kristen Le Liboux
 
Initiation au JavaScript
Initiation au JavaScriptInitiation au JavaScript
Initiation au JavaScript
StrasWeb
 
Javascript proprement
Javascript proprementJavascript proprement
Javascript proprement
Guillaume Collic
 
Javascript pour les Développeurs WEB
Javascript pour les Développeurs WEBJavascript pour les Développeurs WEB
Javascript pour les Développeurs WEB
Abbes Rharrab
 
C1 - Langage C - ISIMA - Première partie
C1 - Langage C - ISIMA - Première partieC1 - Langage C - ISIMA - Première partie
C1 - Langage C - ISIMA - Première partie
Loic Yon
 
[French] Discover haxe
[French] Discover haxe[French] Discover haxe
[French] Discover haxe
Axel Anceau
 
Porte feuilles-perso
Porte feuilles-persoPorte feuilles-perso
Porte feuilles-perso
ymoumen
 

Tendances (18)

TypeScript for dummies
TypeScript for dummiesTypeScript for dummies
TypeScript for dummies
 
Présentation JavaScript
Présentation JavaScriptPrésentation JavaScript
Présentation JavaScript
 
C3 - Langage C - ISIMA 1 - Troisieme partie
C3 - Langage C - ISIMA 1 - Troisieme partieC3 - Langage C - ISIMA 1 - Troisieme partie
C3 - Langage C - ISIMA 1 - Troisieme partie
 
Présentation de ECMAScript 6
Présentation de ECMAScript 6Présentation de ECMAScript 6
Présentation de ECMAScript 6
 
L'Open Web en tant que pierre angulaire du développement multi-objets
L'Open Web en tant que pierre angulaire du développement multi-objetsL'Open Web en tant que pierre angulaire du développement multi-objets
L'Open Web en tant que pierre angulaire du développement multi-objets
 
Cours javascript v1
Cours javascript v1Cours javascript v1
Cours javascript v1
 
Découverte d'UNIX - ISIMA
Découverte d'UNIX - ISIMADécouverte d'UNIX - ISIMA
Découverte d'UNIX - ISIMA
 
Introduction TypeScript
Introduction TypeScriptIntroduction TypeScript
Introduction TypeScript
 
Introduction à JavaScript
Introduction à JavaScriptIntroduction à JavaScript
Introduction à JavaScript
 
Javascript un langage supérieur
Javascript un langage supérieurJavascript un langage supérieur
Javascript un langage supérieur
 
Notions de base de JavaScript
Notions de base de JavaScriptNotions de base de JavaScript
Notions de base de JavaScript
 
Cours php
Cours phpCours php
Cours php
 
Initiation au JavaScript
Initiation au JavaScriptInitiation au JavaScript
Initiation au JavaScript
 
Javascript proprement
Javascript proprementJavascript proprement
Javascript proprement
 
Javascript pour les Développeurs WEB
Javascript pour les Développeurs WEBJavascript pour les Développeurs WEB
Javascript pour les Développeurs WEB
 
C1 - Langage C - ISIMA - Première partie
C1 - Langage C - ISIMA - Première partieC1 - Langage C - ISIMA - Première partie
C1 - Langage C - ISIMA - Première partie
 
[French] Discover haxe
[French] Discover haxe[French] Discover haxe
[French] Discover haxe
 
Porte feuilles-perso
Porte feuilles-persoPorte feuilles-perso
Porte feuilles-perso
 

Similaire à Interface texte plein écran en Go avec TView

Retour d'expérience technique Go, gRPC, Kubernetes
Retour d'expérience technique Go, gRPC, KubernetesRetour d'expérience technique Go, gRPC, Kubernetes
Retour d'expérience technique Go, gRPC, Kubernetes
Vincent Composieux
 
openFrameworks
openFrameworksopenFrameworks
openFrameworks
LeStudioiInteractif
 
Développement avec Java Micro Edition
Développement avec Java Micro EditionDéveloppement avec Java Micro Edition
Développement avec Java Micro Edition
Sylvain Wallez
 
Big Data Viz (and much more!) with Apache Zeppelin
Big Data Viz (and much more!) with Apache ZeppelinBig Data Viz (and much more!) with Apache Zeppelin
Big Data Viz (and much more!) with Apache Zeppelin
Bruno Bonnin
 
Go
GoGo
Node.js, le pavé dans la mare
Node.js, le pavé dans la mareNode.js, le pavé dans la mare
Node.js, le pavé dans la mare
Valtech
 
12-Factor
12-Factor12-Factor
12-Factor
Luc Juggery
 
HTML5 en projet
HTML5 en projetHTML5 en projet
HTML5 en projet
Normandy JUG
 
Rich Desktop Applications
Rich Desktop ApplicationsRich Desktop Applications
Rich Desktop Applications
goldoraf
 
Vert.x 3
Vert.x 3Vert.x 3
Vert.x 3
Xavier MARIN
 
web-avance-jssvghjjjjjjkkkkhjjjjjkrtyujj
web-avance-jssvghjjjjjjkkkkhjjjjjkrtyujjweb-avance-jssvghjjjjjjkkkkhjjjjjkrtyujj
web-avance-jssvghjjjjjjkkkkhjjjjjkrtyujj
CdricMboutou
 
Compte rendu Blend Web Mix 2015
Compte rendu Blend Web Mix 2015Compte rendu Blend Web Mix 2015
Compte rendu Blend Web Mix 2015
SQLI DIGITAL EXPERIENCE
 
Introduction au langage Go
Introduction au langage GoIntroduction au langage Go
Introduction au langage Go
Sylvain Wallez
 
Présentation de Django @ Orange Labs (FR)
Présentation de Django @ Orange Labs (FR)Présentation de Django @ Orange Labs (FR)
Présentation de Django @ Orange Labs (FR)
Martin Latrille
 
Native script
Native scriptNative script
Native script
Neticoa Sénégal
 
Réu technodejs
Réu technodejsRéu technodejs
Réu technodejs
naholyr
 
Firefox OS de la théorie à la pratique - OSDC
Firefox OS de la théorie à la pratique - OSDCFirefox OS de la théorie à la pratique - OSDC
Firefox OS de la théorie à la pratique - OSDC
Christophe Villeneuve
 
Introduction webextensions
Introduction webextensionsIntroduction webextensions
Introduction webextensions
Christophe Villeneuve
 
Web-In 2010: Programmation Native iOS (French)
Web-In 2010: Programmation Native iOS (French)Web-In 2010: Programmation Native iOS (French)
Web-In 2010: Programmation Native iOS (French)
Fred Brunel
 
Comment développer un serveur métier en python/C++
Comment développer un serveur métier en python/C++Comment développer un serveur métier en python/C++
Comment développer un serveur métier en python/C++
cppfrug
 

Similaire à Interface texte plein écran en Go avec TView (20)

Retour d'expérience technique Go, gRPC, Kubernetes
Retour d'expérience technique Go, gRPC, KubernetesRetour d'expérience technique Go, gRPC, Kubernetes
Retour d'expérience technique Go, gRPC, Kubernetes
 
openFrameworks
openFrameworksopenFrameworks
openFrameworks
 
Développement avec Java Micro Edition
Développement avec Java Micro EditionDéveloppement avec Java Micro Edition
Développement avec Java Micro Edition
 
Big Data Viz (and much more!) with Apache Zeppelin
Big Data Viz (and much more!) with Apache ZeppelinBig Data Viz (and much more!) with Apache Zeppelin
Big Data Viz (and much more!) with Apache Zeppelin
 
Go
GoGo
Go
 
Node.js, le pavé dans la mare
Node.js, le pavé dans la mareNode.js, le pavé dans la mare
Node.js, le pavé dans la mare
 
12-Factor
12-Factor12-Factor
12-Factor
 
HTML5 en projet
HTML5 en projetHTML5 en projet
HTML5 en projet
 
Rich Desktop Applications
Rich Desktop ApplicationsRich Desktop Applications
Rich Desktop Applications
 
Vert.x 3
Vert.x 3Vert.x 3
Vert.x 3
 
web-avance-jssvghjjjjjjkkkkhjjjjjkrtyujj
web-avance-jssvghjjjjjjkkkkhjjjjjkrtyujjweb-avance-jssvghjjjjjjkkkkhjjjjjkrtyujj
web-avance-jssvghjjjjjjkkkkhjjjjjkrtyujj
 
Compte rendu Blend Web Mix 2015
Compte rendu Blend Web Mix 2015Compte rendu Blend Web Mix 2015
Compte rendu Blend Web Mix 2015
 
Introduction au langage Go
Introduction au langage GoIntroduction au langage Go
Introduction au langage Go
 
Présentation de Django @ Orange Labs (FR)
Présentation de Django @ Orange Labs (FR)Présentation de Django @ Orange Labs (FR)
Présentation de Django @ Orange Labs (FR)
 
Native script
Native scriptNative script
Native script
 
Réu technodejs
Réu technodejsRéu technodejs
Réu technodejs
 
Firefox OS de la théorie à la pratique - OSDC
Firefox OS de la théorie à la pratique - OSDCFirefox OS de la théorie à la pratique - OSDC
Firefox OS de la théorie à la pratique - OSDC
 
Introduction webextensions
Introduction webextensionsIntroduction webextensions
Introduction webextensions
 
Web-In 2010: Programmation Native iOS (French)
Web-In 2010: Programmation Native iOS (French)Web-In 2010: Programmation Native iOS (French)
Web-In 2010: Programmation Native iOS (French)
 
Comment développer un serveur métier en python/C++
Comment développer un serveur métier en python/C++Comment développer un serveur métier en python/C++
Comment développer un serveur métier en python/C++
 

Plus de OSInet

Scaling up and accelerating Drupal 8 with NoSQL
Scaling up and accelerating Drupal 8 with NoSQLScaling up and accelerating Drupal 8 with NoSQL
Scaling up and accelerating Drupal 8 with NoSQL
OSInet
 
Mon site web est hacké ! Que faire ?
Mon site web est hacké ! Que faire ?Mon site web est hacké ! Que faire ?
Mon site web est hacké ! Que faire ?
OSInet
 
Faster Drupal sites using Queue API
Faster Drupal sites using Queue APIFaster Drupal sites using Queue API
Faster Drupal sites using Queue API
OSInet
 
Life after the hack
Life after the hackLife after the hack
Life after the hack
OSInet
 
Delayed operations with queues for website performance
Delayed operations with queues for website performanceDelayed operations with queues for website performance
Delayed operations with queues for website performance
OSInet
 
Drupal 8 : regards croisés
Drupal 8 : regards croisésDrupal 8 : regards croisés
Drupal 8 : regards croisés
OSInet
 
Cache speedup with Heisencache for Drupal 7 and Drupal 8
Cache speedup with Heisencache for Drupal 7 and Drupal 8Cache speedup with Heisencache for Drupal 7 and Drupal 8
Cache speedup with Heisencache for Drupal 7 and Drupal 8
OSInet
 
Recueil des mauvaises pratiques constatées lors de l'audit de sites Drupal 7
Recueil des mauvaises pratiques constatées lors de l'audit de sites Drupal 7Recueil des mauvaises pratiques constatées lors de l'audit de sites Drupal 7
Recueil des mauvaises pratiques constatées lors de l'audit de sites Drupal 7
OSInet
 
Le groupe PHP-FIG et les standards PSR
Le groupe  PHP-FIG et les standards PSRLe groupe  PHP-FIG et les standards PSR
Le groupe PHP-FIG et les standards PSR
OSInet
 
Les blocs Drupal de drop.org à Drupal 8
Les blocs Drupal de drop.org à Drupal 8Les blocs Drupal de drop.org à Drupal 8
Les blocs Drupal de drop.org à Drupal 8
OSInet
 
Utiliser drupal
Utiliser drupalUtiliser drupal
Utiliser drupal
OSInet
 
Equipe drupal
Equipe drupalEquipe drupal
Equipe drupal
OSInet
 
Pourquoi choisir un CMS Open Source ?
Pourquoi choisir un CMS Open Source ?Pourquoi choisir un CMS Open Source ?
Pourquoi choisir un CMS Open Source ?
OSInet
 
Drupal et le NoSQL - drupagora 2011
Drupal et le NoSQL - drupagora 2011Drupal et le NoSQL - drupagora 2011
Drupal et le NoSQL - drupagora 2011
OSInet
 
Drupal Views development
Drupal Views developmentDrupal Views development
Drupal Views development
OSInet
 

Plus de OSInet (15)

Scaling up and accelerating Drupal 8 with NoSQL
Scaling up and accelerating Drupal 8 with NoSQLScaling up and accelerating Drupal 8 with NoSQL
Scaling up and accelerating Drupal 8 with NoSQL
 
Mon site web est hacké ! Que faire ?
Mon site web est hacké ! Que faire ?Mon site web est hacké ! Que faire ?
Mon site web est hacké ! Que faire ?
 
Faster Drupal sites using Queue API
Faster Drupal sites using Queue APIFaster Drupal sites using Queue API
Faster Drupal sites using Queue API
 
Life after the hack
Life after the hackLife after the hack
Life after the hack
 
Delayed operations with queues for website performance
Delayed operations with queues for website performanceDelayed operations with queues for website performance
Delayed operations with queues for website performance
 
Drupal 8 : regards croisés
Drupal 8 : regards croisésDrupal 8 : regards croisés
Drupal 8 : regards croisés
 
Cache speedup with Heisencache for Drupal 7 and Drupal 8
Cache speedup with Heisencache for Drupal 7 and Drupal 8Cache speedup with Heisencache for Drupal 7 and Drupal 8
Cache speedup with Heisencache for Drupal 7 and Drupal 8
 
Recueil des mauvaises pratiques constatées lors de l'audit de sites Drupal 7
Recueil des mauvaises pratiques constatées lors de l'audit de sites Drupal 7Recueil des mauvaises pratiques constatées lors de l'audit de sites Drupal 7
Recueil des mauvaises pratiques constatées lors de l'audit de sites Drupal 7
 
Le groupe PHP-FIG et les standards PSR
Le groupe  PHP-FIG et les standards PSRLe groupe  PHP-FIG et les standards PSR
Le groupe PHP-FIG et les standards PSR
 
Les blocs Drupal de drop.org à Drupal 8
Les blocs Drupal de drop.org à Drupal 8Les blocs Drupal de drop.org à Drupal 8
Les blocs Drupal de drop.org à Drupal 8
 
Utiliser drupal
Utiliser drupalUtiliser drupal
Utiliser drupal
 
Equipe drupal
Equipe drupalEquipe drupal
Equipe drupal
 
Pourquoi choisir un CMS Open Source ?
Pourquoi choisir un CMS Open Source ?Pourquoi choisir un CMS Open Source ?
Pourquoi choisir un CMS Open Source ?
 
Drupal et le NoSQL - drupagora 2011
Drupal et le NoSQL - drupagora 2011Drupal et le NoSQL - drupagora 2011
Drupal et le NoSQL - drupagora 2011
 
Drupal Views development
Drupal Views developmentDrupal Views development
Drupal Views development
 

Interface texte plein écran en Go avec TView

  • 1. TwinUI Une interface texte plein écran et une interface Web dans une même application
  • 2. https://osinet.fr/go Frédéric G. MARAND Ingénieur conseil en performance logicielle
  • 3. 32 TABLE OF CONTENTS Le besoin Pourquoi 2 interfaces ? Choix techniques Quels modules ? Pourquoi ? TView Comment TView est-il conçu? 1 54 Code Comment coder son UI texte ? Combinaison Comment assembler les 2 UIs ?
  • 5. Le besoin: pourquoi 2 interfaces ? ● L’interface Web est la référence pour les consommateurs ● La ligne de commande est le quotidien des développeurs
  • 6. Le besoin: pourquoi 2 interfaces ? ● Lancer un programme graphique (IDE…) ● Chercher les options d’un programme ● Assembler un pipeline pour un script ● Utiliser vim ou emacs, atop/htop ... ● Administrer une instance en SSH ● Contrôler un programme en cours (docker)
  • 7. Le besoin: pourquoi 2 interfaces ? ● Exemple: “un livre dont vous êtes le héros” aka “choose your own adventure” ● L’original: un projet de Ion Calhoun sur son site Gophercises
  • 8. Le besoin: pourquoi 2 interfaces ? https://gophercises.com/
  • 9. Le besoin: pourquoi 2 interfaces ?
  • 11. Choix techniques: côté Web - pour cette démo ● Pourquoi un framework web plutôt qu’un autre ? ● Aucun: bien pour un micro-service, rarement au-delà ○ Chemins dynamiques ○ Chargeurs / validateurs d’arguments ○ Filtres par méthode ○ Montages de sous-groupes ● Gorilla/mux: ○ minimal ○ popularité maximale ○ modèle des middlewares ○ utilisé pour l’exemple, facile à convertir
  • 12. Choix techniques: côté Web - pour un projet lourd ● Gin https://gin-gonic.com/ ○ plutôt orienté APIs ○ rapide, simple ● Beego https://beego.me/ ○ inspiration Django, ○ simple, bien outillé (ORM, ligne de commande) ● Buffalo https://gobuffalo.io/fr/ ○ pour les projets Web classiques ○ riche: adoption de composants tiers ○ ORM Pop, moteur de templates Plush, I18n… ○ documentation en français ● Revel http://revel.github.io/ ○ Positionnement Buffalo mais depuis 2013
  • 13. Choix techniques: côté texte - l’offre ● Préhistoire: https://github.com/gbin/goncurses ● nsf/termbox-go : ncurses repensé en Go. Très populaire. Abandonné. ● gdamore/tcell : le successeur. Vitesse, Unicode, couleurs, souris. ● gizak/termui : graphique pour tableaux de bord comme expvarmon ● JoelOtter/termloop : spécialisé pour les jeux plein écran texte ● jroimartin/gocui : gestion de “fenêtres” texte basique ● rivo/tview : widgets, fenêtres, application, layout (flex!), 256 couleurs, souris, son, Unicode, basé sur tcell. ● VladimirMarkelov/clui: TurboVision TNG :-O tview FTW !
  • 14. TView
  • 17. TView: organisation du code ● Créer des Widgets: Button, Text, List … ● Les configurer ○ Configurables par leur champ promu Box: SetTitle, SetBackgroundColor ... ○ Et aussi avec leurs propres méthodes: Checkbox.SetChecked, Table.InsertRow … ● Leur ajouter des handlers d’événement s’ils composent FormItem ○ Soit à l’ajout d’enfants: List.AddItem(pri, sec, run, func()) *List ○ Soit directement: List.SetSelectedFunc(func(int, string, string, rune) *List) ● Créer une application avec NewApplication ● Définir quel widget est la racine de l’arborescence avec SetRoot ● Lui ajouter un handler d’événement par SetInputCapture ● Lancer la boucle événementielle avec la méthode Run.
  • 18. TView: composants package main import "github.com/rivo/tview" func main() { tv := tview.NewButton("Hello, world!") tview.NewApplication().SetRoot(tv, true).Run() }
  • 20. TView: composants personnalisés ● Créer ses composants = implémenter TView.Primitive ○ Blur, Draw, Focus, GetFocusable, GetRect, InputHandler, MouseHandler, SetRect ○ Soit directement, soit en composant un Box comme font les autres ● Plus simplement: ○ Surcharger le Draw d’un composant approprié ○ Intercepter les rafraîchissements par les hooks Application.Draw: SetBeforeDrawFunc(), SetAfterDrawFunc() ○ Intercepter les événements ■ Globalement: Application.SetInputCapture() ■ Sur une Primitive: Box.SetInputCapture()
  • 22. Coder ses UI: web Layout: ● arc.gohtml ● gopher.json ● main.go ● README.md ● story.go ● style.css ● web.go Un même package main: ● Template ● Données ● Point d’entrée de l’application ● Documentation ● Modèle de données ● Styles ● Handlers web
  • 23. Coder ses UI: web - main.go package main import ( "flag" "html/template" "log" "net/http" "os" "strconv" "github.com/gorilla/mux" ) func main() { path := flag.String("story", "gopher.json", "The name of the file containing the data") port := flag.Int("port", 8080, "TCP port") flag.Parse() story, err := loadStory(*path) if err != nil { log.Fatalln(err) } //...suite... tpl, err := template.ParseFiles("arc.gohtml") if err != nil { log.Println("Failed parsing arc template", err) os.Exit(2) } r := mux.NewRouter() r.HandleFunc("/style.css", styleHandler) r.HandleFunc("/arc/{arc}", func(w http.ResponseWriter, r *http.Request) { arcHandler(w, r, story, tpl) }) r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/arc/intro", http.StatusMovedPermanently) }) _ = http.ListenAndServe(":"+strconv.Itoa(*port), r) }
  • 24. Coder ses UI: web - arc.gohtml <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Choose Your Own Adventure: {{.Title}}</title> <link rel="stylesheet" href="/style.css"/> </head> <body> <section class="page"> <h1>{{ .Title }}</h1> {{ range .Story }} <p>{{ . }}</p> {{ end }} {{ if .Options }} <ul> {{ range .Options }} <li><a href="/arc/{{ .ArcName }}"> {{ .Text }}</a></li> {{ end }} </ul> {{ end }} </section> <footer> This is a demo of an exercise from the free course <a href="https://gophercises.com"> Gophercises</a>. Check it out if you are interested in learning / practicing Go. </footer> </body> </html>
  • 25. Coder ses UI: web - story.go package main import ( "encoding/json" "os" ) // Option represents a choice at the end of an arc. type Option struct { Text string ArcName string `json:"arc"` } // Arc represents an individual narrative structure. type Arc struct { Title string Story []string Options []Option } // Story is the graph of arcs. type Story map[string]Arc func loadStory(path string) (Story, error) { story := Story{} file, err := os.Open(path) if err != nil { return story, err } decoder := json.NewDecoder(file) err = decoder.Decode(&story) return story, err }
  • 26. Coder ses UI: web - web.go func arcHandler(w http.ResponseWriter, r *http.Request, story Story, tpl *template.Template) { name, ok := mux.Vars(r)["arc"] if !ok { log.Println("arcHandler called without an arc") http.Redirect(w, r, "/", http.StatusMovedPermanently) return } arc, ok := story[name] if !ok { log.Printf("Incorrect arc requested: %sn", name) http.NotFound(w, r) return } err := tpl.Execute(w, arc) if err != nil { log.Printf("Arc template %#v: %vn", arc, err) w.WriteHeader(http.StatusInternalServerError) return } } func styleHandler(w http.ResponseWriter, r *http.Request) { file, err := os.Open("style.css") if err != nil { log.Println(err) http.NotFound(w, r) return } defer file.Close() w.Header().Set("Content-Type", "text/css") _, err = io.Copy(w, file) if err != nil { log.Println("Failed sending CSS", err) w.WriteHeader( http.StatusInternalServerError) } }
  • 27. Coder ses UI: web - rendu
  • 28. Coder ses UI: terminal - primitives
  • 29. Coder ses UI: terminal - layout Layout: ● ../gophercises_cyoa/gopher.json ● main.go ● model.go ● ui.go Un même package main: ● Données ● Point d’entrée de l’application ● Accès aux données ● Composants d’interface
  • 30. Coder ses UI: terminal - main.go func initUI(story *Story) (*tview.Application, *View) { view := NewView(story) app := tview.NewApplication() app.SetRoot(view.Grid, true). EnableMouse(true). SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { switch event.Key() { case tcell.KeyEsc: app.Stop() case tcell.KeyRune: u := view.URLFromKey(event) switch { case u == `quit`: app.Stop() case u != ``: view.Handle(u) } } return nil }) return app, view } func initModel(path string) (*Story, error) { story := make(Story) err := story.Load(path) if err != nil { return nil, fmt.Errorf(`loading story: %w`, err) } return &story, nil } func main() { path := flag.String(`story`, `./gophercises_cyoa/gopher.json`, ` "The name of the file containing the data`) flag.Parse() story, err := initModel(*path) if err != nil { log.Fatalf("Starting model: %vn", err) } app, view := initUI(story) view.Handle(`intro`) if err := app.Run(); err != nil { log.Fatalf("Running app: %vn", err) } }
  • 31. Coder ses UI: terminal - model.go package main import ( "encoding/json" "fmt" "os" ) // Option represents a choice at the end of an arc. type Option struct { Label string `json:"text"` URL string `json:"arc"` } // Arc represents an individual narrative structure. type Arc struct { Title string `json:"title"` Body []string `json:"story"` Options []Option `json:"options"` } // Story is the graph of arcs. type Story map[string]Arc // Load fetches the story data from disk. func (s *Story) Load(path string) error { file, err := os.Open(path) if err != nil { return fmt.Errorf(`opening %s: %w`, path, err) } decoder := json.NewDecoder(file) err = decoder.Decode(s) return err } // Arc obtains an arc from its URL. func (s *Story) Arc(url string) *Arc { a, ok := (*s)[url] if !ok { return nil } return &a }
  • 32. Coder ses UI: terminal - ui.go package main import ( "fmt" "log" "github.com/gdamore/tcell" "github.com/rivo/tview" ) // View holds the structure of the application View: type View struct { // Heading is the top line. Heading *tview.TextView // Body is the main frame. Body *tview.TextView // Actions contains the action menu. Actions *tview.List // Grid is the container wrapping Heading, Body, Actions. *tview.Grid // Story is the model from which the View reads data. *Story } func (v View) Handle(url string) { arc := v.Story.Arc(url) if arc == nil { log.Printf("Path not found: %sn", url) return } fmt.Fprint(v.Heading.Clear(), arc.Title) b := v.Body.Clear() for _, row := range arc.Body { fmt.Fprintln(b, row + "n") } v.Actions.Clear() if len(arc.Options) == 0 { arc.Options = []Option{{ Label: `Leave story`, URL: `quit`, }} } for k, item := range arc.Options { v.Actions.InsertItem(k, item.Label, item.URL, rune('a' + k), nil) } }
  • 33. Coder ses UI: terminal - ui.go (2) func textView(title string) *tview.TextView { tv := tview.NewTextView(). SetTextAlign(tview.AlignLeft). SetTextColor(tcell.ColorBlack) tv.SetBackgroundColor(tcell.ColorWhite). SetBorderColor(tcell.ColorLightGray). SetBorder(true) tv.SetTitle(` ` + title + ` `). SetTitleColor(tcell.ColorSlateGray). SetTitleAlign(tview.AlignLeft) return tv } func list(title string) *tview.List { l := tview.NewList(). SetMainTextColor(tcell.ColorBlack). ShowSecondaryText(false). SetShortcutColor(tcell.ColorDarkGreen) l.SetBackgroundColor(tcell.ColorWhite). SetBorderColor(tcell.ColorLightGray). SetBorder(true). SetTitle(` ` + title + ` `). SetTitleColor(tcell.ColorSlateGray). SetTitleAlign(tview.AlignLeft) return l } // NewView builds an initialized View. func NewView(story *Story) *View { v := &View{ Heading: textView("Scene"), Body: textView("Description").SetScrollable(true), Actions: list("Choose wisely"), Grid: tview.NewGrid(), Story: story, } v.Grid. SetRows(3, 0, 5). // 1-row title, 3-row actions. Add 2 for their own borders. SetBorders(false). // Use the view borders instead. AddItem(v.Heading, 0, 0, 1, 1, 0, 0, false). AddItem(v.Body, 1, 0, 1, 1, 0, 0, false). AddItem(v.Actions, 2, 0, 1, 1, 0, 0, true) return v }
  • 34. Coder ses UI: web - résultat
  • 36. TwinUI: composer les 2 UIs ● 2 paquets main, chacun dans leur répertoire ● Problème: chacun a sa logique d’accès aux données ● Problème: chacun se termine par une fonction bloquante: ○ Web: _ = http.ListenAndServe(":"+strconv.Itoa(*port), r) ○ TView: _ = app.Run() ● Refactoring !
  • 37. TwinUI: composer les 2 UIs Layout: ● main.go ● model/ ○ gopher.json ○ model.go ● tview/ ○ ui.go ● web/ ○ arc.gohtml ○ style.css ○ web.go 4 paquets (...mais…) ● Point d’entrée de l’application ● Source de données ○ Données ○ Accès aux données ● Interacteur: terminal ○ Composants texte ● Interacteur: web ○ Template ○ Styles ○ Composants web
  • 38. TwinUI: composer les 2 UIs Si ça vous rappelle quelque chose, ce n’est pas un hasard… À l’échelle d’un projet réel, la généralisation est souvent représentée comme ceci et appelée “architecture hexagonale”. Image: Blog Netflix https://netflixtechblog.com/ready-for- changes-with-hexagonal-architecture- b315ec967749
  • 39. TwinUI: le secret est dans le main func main() { path := flag.String(`story`, `./model/gopher.json`, `The name of the data file`) port := flag.Int("port", 8080, "The TCP port on which to listen") flag.Parse() // Initialize model: without data, we can't proceed. story, err := initModel(*path) if err != nil { log.Fatalf("Starting model: %vn", err) } defer story.Close() // Initialize the twin UIs. app := initTextUI(story) router := initWebUI(story) ....
  • 40. TwinUI: le secret est dans le main …. // Run the twin UIs, exiting the app whenever either of them exits. done := make(chan bool) go func() { if err := app.Run(); err != nil { log.Fatalf("Running text app: %vn", err) } done <- true }() go func() { if err := http.ListenAndServe(":"+strconv.Itoa(*port), router); err != nil { log.Fatalf("Running web app: %vn", err) } done <- true }() <-done
  • 41. TwinUI: one last thing... ● Et les erreurs ? log utilise la sortie d’erreur, donc le terminal ○ Mais nous avons déjà une UI plein écran en cours
  • 42. TwinUI: one last thing... ● tview.TermView implémente io.Writer: ● log.SetOutput(&logger{app: app, Writer: view.Body})

Notes de l'éditeur

  1. Question à l’audience ? Qui utilise la ligne de commande pour se connecter à ses serveurs ? A ses conteneurs / VMs ?
  2. Question à l’audience ? Qui utilise la ligne de commande pour se connecter à ses serveurs ? A ses conteneurs / VMs ?
  3. Question à l’audience ? Qui utilise la ligne de commande pour se connecter à ses serveurs ? A ses conteneurs / VMs ?
  4. Question à l’audience ? Qui utilise la ligne de commande pour se connecter à ses serveurs ? A ses conteneurs / VMs ?
  5. Question à l’audience ? Qui utilise la ligne de commande pour se connecter à ses serveurs ? A ses conteneurs / VMs ?
  6. Pour quelque chose proche de PreventDefault + StopPropagation: renvoyer nil en sortie de SetInputCapture