[Go language] Vous pouvez créer une application TUI avec Elm Architecture Créez une application ToDo légèrement riche avec bubbletea

Préface

Dans cet article, nous allons créer une application ToDo en utilisant un framework appelé bubbletea qui vous permet de créer une application TUI dans Elm Architecture comme.

Vous pouvez afficher, ajouter, modifier, terminer et supprimer des tâches / tâches terminées. Image image ↓ イメージ画像

À propos d'Elm Architecture

Pour ceux qui ne connaissent pas Elm Architecture, Guide officiel (traduction japonaise) et [Cet article](https://qiita.com/kazurego7/items/ Je pense que c'est plus facile à comprendre si vous lisez brièvement 27a2b6f8b4a1bfac4bd3). (Cet article décrit rarement l'architecture Elm.)

J'ai un petit contact avec Elm, donc j'ai une faible compréhension de l'architecture Elm. Donc, si vous avez des erreurs, veuillez nous en informer dans les commentaires ou sur Twitter.

Fonctionnalités implémentées dans cet article

Dans cet article, je vais essayer d'afficher la liste des tâches et d'expliquer (guide) jusqu'à l'ajout de tâches. De là, je viens d'ajouter du code. Yuzuy / todo-cli pour ceux qui veulent lire uniquement le code et ressentir l'atmosphère, ceux qui veulent savoir en détail comment implémenter d'autres fonctions, et ceux qui lancent Masakari Voir todo-cli)!

Commençons à l'implémenter maintenant!

la mise en oeuvre

Environnement de l'écrivain

version
OS macOS Catalina 10.15.7
iTerm 3.3.12
Go 1.15.2
bubbletea 0.7.0
bubbles 0.7.0

Remarque: à propos du thé à bulles. Il y a eu des changements destructeurs lors de la mise en œuvre de cette application, donc si vous rencontrez un problème avec une version différente de bubbletea, veuillez vous référer au Repository. S'il vous plaît.

Liste des tâches

Dans cette section, nous allons afficher la liste des tâches et l'implémenter au point où vous pouvez sélectionner une tâche avec le curseur.

Allons chercher le paquet en premier.

// bubbletea
go get github.com/charmbracelet/bubbletea

// utility
go get github.com/charmbracelet/bubbles

Model

Tout d'abord, définissez la structure de la tâche.

main.go


type Task struct {
    ID        int
    Name      string
    IsDone    bool
    CreatedAt time.Time
}

Il a une structure minimale, donc si vous voulez quelque chose d'autre comme «Terminé à», ajoutez-le.

Ensuite, définissez model. C'est le modèle de l'architecture Elm. Elm Architecture gère l'état de l'application avec un modèle. Dans ce chapitre Puisqu'il y a deux états à gérer, la liste des tâches et la position du curseur, l'implémentation est la suivante.

main.go


type model struct {
    cursor int
    tasks  []*Task
}

Cela seul ne traitera pas le thé à bulles comme un modèle. Pour le traiter comme un modèle, vous devez avoir model implémenter tea.Model.

Définition de «modèle de thé» ↓

// Model contains the program's state as well as it's core functions.
type Model interface {
	// Init is the first function that will be called. It returns an optional
	// initial command. To not perform an initial command return nil.
	Init() Cmd

	// Update is called when a message is received. Use it to inspect messages
	// and, in response, update the model and/or send a command.
	Update(Msg) (Model, Cmd)

	// View renders the program's UI, which is just a string. The view is
	// rendered after every Update.
	View() string
}

Tout d'abord, nous allons l'implémenter à partir de la fonction d'initialisation ʻInit () , mais comme il n'y a pas de commande à exécuter en premier dans cette application, il est normal de renvoyer simplement nil. (L'initialisation de la structure model` se fait séparément.) (Cet article ne traite pas beaucoup des commandes, alors ne vous inquiétez pas trop des commandes. Si vous êtes intéressé, Elm Official Guide (Japanese translation) Vous voudrez peut-être vous référer à /).)

main.go


import (
    ...
    tea "github.com/charmbracelet/bubbletea"
)

...

func (m model) Init() tea.Cmd {
    return nil
}

Update

ʻUpdate () change l'état de model` en fonction de l'opération de l'utilisateur (Msg).

main.go


func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    switch msg := msg.(type) {
    case tea.KeyMsg:
        switch msg.String() {
        case "j":
            if m.cursor < len(m.tasks) {
                m.cursor++
            }
        case "k":
            if m.cursor > 1 {
                m.cursor--
            }
        case "q":
            return m, tea.Quit
        }
    }

    return m, nil
}

Manipulation de tea.KeyMsg (opération par clé utilisateur), Lorsque vous appuyez sur "j", la valeur du curseur est incrémentée de 1. Diminuez la valeur du curseur de 1 lorsque vous appuyez sur "k". Il définit que lorsque "q" est pressé, il renvoie la commande tea.Quit pour quitter l'application.

Comme c'est un problème si le curseur monte et descend indéfiniment, des conditions telles que «m.cursor> 1» sont ajoutées. Au fait, si vous utilisez case" j "," down " ou case" k "," up ", vous pouvez déplacer le curseur de haut en bas avec les touches fléchées, veuillez donc l'utiliser comme vous le souhaitez.

View

View () génère le texte à dessiner basé sur model. Je vais l'écrire avec string.

main.go


func (m model) View() string {
    s := "--YOUR TASKS--\n\n"

    for i, v := range m.tasks {
        cursor := " "
        if i == m.cursor-1 {
            cursor = ">"
        }

        timeLayout := "2006-01-02 15:04"
        s += fmt.Sprintf("%s #%d %s (%s)\n", cursor, v.ID, v.Name, v.CreatedAt.Format(timeLayout))
    }

    s += "\nPress 'q' to quit\n"

    return s
}

Puisque le curseur n'est pas compté par le numéro d'index, il est jugé en comparant les index «i» et «m.cursor-1» si le curseur pointe vers la tâche.

Vous disposez désormais de suffisamment de matériel pour visualiser vos tâches! Définissons la fonction main pour que l'application puisse être démarrée!

main

La fonction main initialise la structure model et démarre l'application.

main.go


func main() {
    m := model{
        cursor: 1,
        tasks:  []*Task{
            {
                ID:        1,
                Name:      "First task!",
                CreatedAt: time.Now(),
            },
            {
                ID:        2,
                Name:      "Write an article about bubbletea",
                CreatedAt: time.Now(),
            },
        }
    }

    p := tea.NewProgram(m)
    if err := p.Start(); err != nil {
        fmt.Printf("app-name: %s", err.Error())
        os.Exit(1)
    }
}

Normalement, les tâches sont lues à partir de fichiers, etc., mais leur implémentation prend du temps, donc cette fois nous allons les coder en dur. Générez un programme avec tea.NewProgram () et démarrez-le avec p.Start ().

Lançons-le immédiatement avec la commande go run! Une liste de tâches devrait être affichée et vous devriez pouvoir déplacer le curseur de haut en bas avec les touches «j» et «k»!

Ajouter une tâche

Eh bien, j'ai pu afficher la liste des tâches, mais avec cela, il est peu probable que l'application ToDo soit nommée. Dans cette section, nous mettrons en œuvre l'une des fonctionnalités les plus importantes de l'application ToDo, en ajoutant des tâches.

Model

Jusqu'à présent, il était normal de maintenir simplement la position du curseur et la liste des tâches, mais lors de la mise en œuvre de l'ajout d'une tâche, nous devons fournir un champ qui reçoit les entrées de l'utilisateur et les conserve.

bubbleteaでテキスト入力を実装するにはgithub.com/charmbracelet/bubbles/textinputというパッケージを利用します。

main.go


import (
    ...
    input "github.com/charmbracelet/bubbles/textinput"
)

type model struct {
    ...
    newTaskNameInput input.Model
}

Puisqu'il n'est pas possible de faire la distinction entre le mode d'affichage de la liste de tâches (ci-après dénommé mode normal) et le mode d'ajout de tâches (ci-après dénommé mode supplémentaire) par ce seul, un champ appelé «mode» est également ajouté.

main.go


type model struct {
    mode int
    ...
    newTaskNameInput input.Model
}

Définissons également l'identifiant mode.

main.go


const (
    normalMode = iota
    additionalMode
)

Update

Je voudrais commencer à changer la fonction ʻUpdate () `immédiatement, mais il manque un élément lors de l'ajout d'une tâche. Cette application ToDo souhaite gérer les identifiants de tâches dans l'ordre, vous devez donc conserver les derniers identifiants de tâches.

~~ Déclarer comme une variable globale, initialiser avec main (), incrémenter avec ʻUpdate () , et ainsi de suite. (J'ai réfléchi en écrivant, mais il peut être géré comme un champ modèle.) ~~ [Twitter](https://twitter.com/ababupdownba/status/1320139661579218945?s=20) signalé par [@ababupdownba](https://twitter.com/ababupdownba), après tout model` Il vaut mieux en faire un champ, alors je vais l'implémenter là-bas.

main.go


type model struct {
    ...
    latestTaskID int
}

func main() {
    ...
    //Il s'agit d'un festival de codage en dur, mais si vous voulez l'implémenter correctement, entrez la valeur initiale lors de la lecture d'un fichier, etc.
    m.latestTaskID = 2
    ...
}

Par ailleurs, concernant ʻUpdate () `du sujet principal, dans le mode supplémentaire, contrairement au mode normal, toutes les touches de caractères sont traitées comme des entrées.

Par conséquent, définissez model.addingTaskUpdate () séparément demodel.Update (), et si model.mode == additionalMode, traitez-le ici.

En mode normal, il passe en mode supplémentaire lorsque "a" est enfoncé. En mode ajout, assurons-nous que la tâche est ajoutée lorsque vous appuyez sur "Entrée", afin qu'elle revienne en mode normal lorsque vous appuyez sur "ctrl + q".

main.go


func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    if m.mode == additionalMode {
        return m.addingTaskUpdate(msg)
    }

    switch msg := msg.(type) {
    case tea.KeyMsg:
        switch msg.String() {
        ...
        case "a":
            m.mode = additionalMode
        ...
    }

    return m, nil
}

func (m model) addingTaskUpdate(msg tea.Msg) (tea.Model, tea.Cmd) {
    switch msg := msg.(type) {
    case tea.KeyMsg:
        switch msg.String() {
        case "ctrl+q":
            m.mode = normalMode
            m.newTaskNameInput.Reset()
            return m, nil
        case "enter":
            m.latestTaskID++
            m.tasks = append(m.tasks, &Task{
                ID  :      m.latestTaskID,
                Name:      m.newTaskNameInput.Value(),
                CreatedAt: time.Now(),
            })

            m.mode = normalMode
            m.newTaskNameInput.Reset()

            return m, nil
        }
    }

    var cmd tea.Cmd
    m.newTaskNameInput, cmd = input.Update(msg, m.newTaskNameInput)

    return m, cmd
}

La valeur saisie avec «m.newTaskNameInput.Value ()» est récupérée et réinitialisée avec «m.newTaskNameInput.Reset ()». ʻInput.Update () gère les frappes et met à jour m.newTaskNameInput`.

View

Vous devez séparer le même processus que ʻUpdate () de View () `. Mais cela peut être implémenté avec seulement 6 changements de ligne!

main.go


func (m model) View() string {
    if m.mode == additionalMode {
        return m.addingTaskView()
    }
    ...
}

func (m model) addingTaskView() string {
    return fmt.Sprintf("Additional Mode\n\nInput a new task name\n\n%s", input.View(m.newTaskNameInput))
}

ʻInput.View () est une variable qui convertit ʻinput.Model en string pourView ().

main

Ajoutons l'initialisation des nouveaux champs mode et newTaskNameInput.

main.go


func main() {
    newTaskNameInput := input.NewModel()
    newTaskNameInput.Placeholder = "New task name..."
    newTaskNameInput.Focus()

    m := model{
        mode: normalMode,
        ...
        newTaskNameInput: newTaskNameInput,
    }
    ...
}

«Placeholder» est une chaîne de caractères à afficher lorsque rien n'est entré.

Appeler Focus () focalisera ce ʻinput.Model`. Cela semble être utilisé lors de la gestion de plusieurs entrées sur un seul écran. Cette fois, nous ne vous laisserons pas entrer plus d'un, donc je pense que ce n'est pas grave si vous pensez que c'est une magie.

Vous avez maintenant implémenté l'ajout de tâches! Exécutons-le avec la commande go run etc. comme avant!

Épilogue

Dans cet article, j'ai expliqué comment créer une application ToDo légèrement riche en utilisant bubbletea. C'était plus facile à mettre en œuvre que ce à quoi je m'attendais, et je voulais créer un outil CLI complexe comme tig, mais pour moi qui ne pouvais pas faire le premier pas. C'était un cadre très attrayant.

bubbletea possède de nombreuses autres fonctionnalités pour créer des applications TUI riches, alors assurez-vous de consulter le Repository!

Recommended Posts

[Go language] Vous pouvez créer une application TUI avec Elm Architecture Créez une application ToDo légèrement riche avec bubbletea
Essayez de créer une application Todo avec le framework Django REST
Créer une application Todo avec Django ③ Créer une page de liste de tâches
Créer une application Todo avec Django ⑤ Créer une fonction d'édition de tâches
Créer une application Todo avec Django ① Créer un environnement avec Docker
Créer une application Todo avec Django ④ Implémenter la fonction de création de dossier et de tâche
Créer une application graphique avec Tkinter de Python
Créez une application Web simple avec Flask
Procédure de création d'application multi-plateforme avec kivy
[Pratique] Créez une application Watson avec Python! # 1 [Discrimination linguistique]