écrits/tutorial/2026/07
Tutorial3 juil. 2026·26 min

Créer des interfaces terminal avec OpenTUI et React en TypeScript

Apprenez à construire des applications terminal rapides et basées sur des composants avec OpenTUI — le framework TUI en TypeScript qui propulse opencode. Ce guide pratique couvre l'installation, les bindings React, la mise en page flexbox, la saisie clavier et un vrai tableau de bord de monitoring système.

Les interfaces en terminal connaissent une renaissance. Des outils comme lazygit, k9s et les agents de codage IA tels que opencode ont montré qu'une TUI bien conçue peut être plus rapide et plus concentrée qu'un onglet de navigateur. OpenTUI est le framework derrière plusieurs de ces expériences : une bibliothèque TypeScript avec un cœur de rendu natif écrit en Zig, une architecture par composants, une mise en page flexbox et des bindings de première classe pour React et Solid.

Dans ce tutoriel, vous allez construire un véritable tableau de bord terminal avec les bindings React d'OpenTUI — le même modèle déclaratif que vous connaissez déjà du web, mais rendu dans le terminal plutôt que dans le DOM. À la fin, vous comprendrez la boucle de rendu, le système de mise en page, la gestion du clavier et les hooks d'animation suffisamment bien pour livrer vos propres outils CLI.

Prérequis

Avant de commencer, assurez-vous d'avoir :

  • Bun 1.1+ installé (bun --version) — le cœur natif d'OpenTUI se résout le plus vite sur Bun, mais Node 20+ fonctionne aussi
  • De l'aisance avec TypeScript et les hooks React (useState, useEffect)
  • Un terminal qui prend en charge le truecolor (la plupart des terminaux modernes le font)
  • Un éditeur de code — VS Code est recommandé

Vous n'avez pas besoin de connaître Zig. Le cœur natif est livré sous forme de binaire précompilé et vous interagissez avec lui entièrement via TypeScript.

Ce que vous allez construire

À la fin de ce tutoriel, vous aurez un tableau de bord de monitoring système en direct qui s'exécute dans votre terminal, comprenant :

  1. Une mise en page bordée divisée en panneaux avec flexbox.
  2. Un en-tête rendu avec une grande police ASCII.
  3. Des statistiques de mémoire et de charge mises à jour en temps réel.
  4. Un menu sélectionnable navigable au clavier.
  5. Une barre de progression animée fluide pilotée par la timeline d'OpenTUI.
  6. Un arrêt propre en appuyant sur la touche q.

Étape 1 : Configuration du projet

Créez un projet neuf et installez le cœur ainsi que le binding React. OpenTUI est découpé en petits paquets pour que vous n'importiez que ce que vous utilisez.

mkdir opentui-dashboard && cd opentui-dashboard
bun init -y
bun add @opentui/core @opentui/react react

Les deux paquets qui vous intéressent sont :

  • @opentui/core — le renderer, le moteur de mise en page et les composants renderables de bas niveau
  • @opentui/react — le réconciliateur qui vous permet de décrire votre interface avec du JSX

Ajoutez un script dev à package.json pour lancer l'application avec rechargement à chaud :

{
  "scripts": {
    "dev": "bun --hot run src/index.tsx"
  }
}

Étape 2 : Votre premier rendu

OpenTUI a besoin de deux choses : un renderer (qui possède le terminal, le frame buffer et le flux d'entrée) et un root (qui réconcilie votre arbre React dans ce renderer). Créez src/index.tsx :

/** @jsxImportSource @opentui/react */
import { createCliRenderer } from "@opentui/core"
import { createRoot } from "@opentui/react"
 
function App() {
  return (
    <box style={{ border: true, padding: 1 }}>
      <text>Bonjour depuis le terminal</text>
    </box>
  )
}
 
const renderer = await createCliRenderer()
createRoot(renderer).render(<App />)

Lancez-le :

bun run dev

Vous devriez voir une boîte bordée avec votre message. Quelques points méritent d'être notés ici. Le pragma jsxImportSource en haut indique au compilateur d'utiliser le runtime JSX d'OpenTUI au lieu de React DOM — c'est ce qui associe <box> et <text> aux renderables du terminal. Les éléments intrinsèques sont en minuscules (box, text, input, select), et chaque élément visuel accepte une prop style.

Astuce : Si vous préférez ne pas écrire le pragma dans chaque fichier, définissez "jsxImportSource": "@opentui/react" dans votre tsconfig.json sous compilerOptions et cela s'applique à tout le projet.

Étape 3 : Mise en page avec Flexbox

Le moteur de mise en page d'OpenTUI est une implémentation flexbox, donc le modèle mental se transfère directement depuis CSS. Les boîtes sont des conteneurs flex ; vous contrôlez la direction, la taille et l'espacement avec des propriétés familières. Divisons l'écran en un en-tête et un corps à deux colonnes.

function Dashboard() {
  return (
    <box style={{ flexDirection: "column", height: "100%" }}>
      <box style={{ border: true, padding: 1 }}>
        <text>MONITEUR SYSTÈME</text>
      </box>
 
      <box style={{ flexDirection: "row", flexGrow: 1 }}>
        <box style={{ border: true, flexGrow: 1, padding: 1 }}>
          <text>Panneau gauche — stats</text>
        </box>
        <box style={{ border: true, flexGrow: 1, padding: 1 }}>
          <text>Panneau droit — menu</text>
        </box>
      </box>
    </box>
  )
}

La boîte extérieure empile les enfants verticalement (flexDirection: "column") et remplit la hauteur du terminal. La boîte du corps place ses deux panneaux côte à côte (flexDirection: "row"), et flexGrow: 1 fait que chaque panneau s'étend pour partager équitablement la largeur disponible. Redimensionnez votre terminal et la mise en page se recalcule automatiquement — aucun calcul manuel requis.

Étape 4 : État en direct avec les hooks React

Comme les bindings React exécutent un vrai réconciliateur, les hooks standard fonctionnent exactement comme vous l'attendez. Ajoutons des statistiques de mémoire et de charge qui se rafraîchissent chaque seconde. Nous les lisons depuis le module os de Node, que Bun expose.

import { useState, useEffect } from "react"
import os from "node:os"
 
function useSystemStats() {
  const [stats, setStats] = useState({ usedPct: 0, load: 0 })
 
  useEffect(() => {
    const tick = () => {
      const total = os.totalmem()
      const free = os.freemem()
      const usedPct = Math.round(((total - free) / total) * 100)
      const load = os.loadavg()[0]
      setStats({ usedPct, load: Number(load.toFixed(2)) })
    }
    tick()
    const id = setInterval(tick, 1000)
    return () => clearInterval(id)
  }, [])
 
  return stats
}

Affichez maintenant ces valeurs dans le panneau gauche. Remarquez comment fg définit la couleur de premier plan et comment nous composons les nœuds de texte tout comme React sur le web.

function StatsPanel() {
  const { usedPct, load } = useSystemStats()
 
  return (
    <box style={{ border: true, flexGrow: 1, padding: 1, flexDirection: "column" }}>
      <text style={{ fg: "#7dd3fc" }}>Mémoire utilisée</text>
      <text>{usedPct}%</text>
      <text style={{ fg: "#c4b5fd" }}>Charge moyenne (1m)</text>
      <text>{load}</text>
    </box>
  )
}

Le panneau se met désormais à jour une fois par seconde. Comme OpenTUI compare l'arbre virtuel et ne repeint que les cellules modifiées, ces mises à jour restent peu coûteuses même à des fréquences élevées.

Étape 5 : Navigation clavier et menu

Les TUI interactives vivent ou meurent selon leur gestion du clavier. OpenTUI fournit un hook useKeyboard pour les événements de touches bruts et un composant <select> pour les menus. D'abord, câblons une touche de sortie globale en utilisant useRenderer pour accéder au renderer actif.

import { useKeyboard, useRenderer } from "@opentui/react"
 
function useQuitOnQ() {
  const renderer = useRenderer()
  useKeyboard((key) => {
    if (key.name === "q" || key.name === "escape") {
      renderer.destroy()
      process.exit(0)
    }
  })
}

Ensuite, ajoutez un menu à droite. Le composant <select> gère la navigation par flèches et émet l'option choisie via onSelect.

function MenuPanel() {
  const [selected, setSelected] = useState("Aperçu")
 
  return (
    <box style={{ border: true, flexGrow: 1, padding: 1, flexDirection: "column" }}>
      <text style={{ fg: "#a7f3d0" }}>Vue — actuelle : {selected}</text>
      <select
        style={{ height: 6 }}
        options={[
          { name: "Aperçu", description: "Résumé de toutes les métriques" },
          { name: "Processus", description: "Principaux processus actifs" },
          { name: "Réseau", description: "Débit des interfaces" },
          { name: "Disque", description: "Utilisation des volumes" },
        ]}
        onSelect={(index, option) => setSelected(option.name)}
      />
    </box>
  )
}

Un <select> doit avoir le focus pour recevoir les touches. Lorsqu'il est le seul élément interactif, OpenTUI lui donne le focus automatiquement ; avec plusieurs widgets focalisables, vous gérez le focus explicitement, ce que nous couvrons dans la section dépannage.

Étape 6 : Progression animée avec la timeline

Les barres statiques suffisent, mais une animation fluide communique l'activité. Le hook useTimeline d'OpenTUI anime n'importe quelle valeur numérique dans le temps avec de l'easing — voyez-le comme un moteur de tweening léger. Ici, nous animons une largeur de remplissage pour visualiser le pourcentage de mémoire.

import { useTimeline } from "@opentui/react"
import { useEffect, useState } from "react"
 
function ProgressBar({ target }: { target: number }) {
  const [width, setWidth] = useState(0)
  const timeline = useTimeline({ duration: 600, loop: false })
 
  useEffect(() => {
    timeline.add(
      { width },
      {
        width: target,
        duration: 600,
        ease: "outCubic",
        onUpdate: (anim) => setWidth(Math.round(anim.targets[0].width)),
      },
    )
  }, [target])
 
  return (
    <box style={{ flexDirection: "row" }}>
      <box style={{ width, height: 1, backgroundColor: "#22d3ee" }} />
      <text> {target}%</text>
    </box>
  )
}

Chaque fois que target change, la timeline anime la barre vers sa nouvelle largeur sur 600 millisecondes avec un ease-out cubique, de sorte que le tableau de bord semble vivant plutôt que de sauter d'une image à l'autre.

Étape 7 : Composer le tableau de bord complet

Assemblez maintenant le tout dans l'arbre final. Le root câble le gestionnaire de sortie, rend l'en-tête ASCII et place les panneaux de stats et de menu côte à côte.

/** @jsxImportSource @opentui/react */
import { createCliRenderer } from "@opentui/core"
import { createRoot } from "@opentui/react"
 
function App() {
  useQuitOnQ()
  const { usedPct } = useSystemStats()
 
  return (
    <box style={{ flexDirection: "column", height: "100%" }}>
      <box style={{ border: true, padding: 1 }}>
        <ascii-font text="MONITOR" font="tiny" />
      </box>
 
      <box style={{ flexDirection: "row", flexGrow: 1 }}>
        <box style={{ flexDirection: "column", flexGrow: 1 }}>
          <StatsPanel />
          <box style={{ border: true, padding: 1 }}>
            <ProgressBar target={usedPct} />
          </box>
        </box>
        <MenuPanel />
      </box>
 
      <box style={{ padding: 1 }}>
        <text style={{ fg: "#94a3b8" }}>Appuyez sur q pour quitter</text>
      </box>
    </box>
  )
}
 
const renderer = await createCliRenderer()
createRoot(renderer).render(<App />)

Lancez bun run dev et vous avez un tableau de bord en direct piloté au clavier : un en-tête ASCII, des stats auto-actualisées, une barre mémoire animée et un menu navigable — le tout en moins de 150 lignes de TypeScript.

Tester votre implémentation

Vérifiez que l'application se comporte correctement :

  1. Recalcul de la mise en page : Redimensionnez la fenêtre du terminal. Les panneaux doivent redistribuer la largeur sans corrompre les bordures.
  2. Mises à jour en direct : Surveillez le pourcentage de mémoire. Ouvrez une application lourde et confirmez que le nombre et la barre réagissent en une seconde.
  3. Navigation : Utilisez les touches fléchées pour parcourir le menu ; l'étiquette « actuelle » doit suivre votre sélection.
  4. Sortie propre : Appuyez sur q. Le terminal doit restaurer votre invite avec le curseur visible et sans artefacts résiduels.

Si le curseur reste caché après la sortie, cela signifie que renderer.destroy() ne s'est pas exécuté — appelez-le toujours avant process.exit.

Dépannage

Sortie brouillée ou couleurs manquantes. Votre terminal n'annonce peut-être pas le truecolor. Définissez COLORTERM=truecolor dans votre environnement, ou repliez-vous sur des styles à 256 couleurs.

Touches non enregistrées. Seuls les widgets focalisés reçoivent les événements de touches. Si vous avez plus d'un élément interactif, appelez .focus() sur la cible visée, ou gérez le focus avec le paquet keymap (@opentui/keymap), qui associe des commandes nommées à des raccourcis et gère les anneaux de focus pour vous.

Scintillement lors de mises à jour rapides. OpenTUI regroupe les rendus par image, mais appeler setState dans une boucle synchrone serrée peut quand même surcharger. Limitez les mises à jour à un intervalle raisonnable — une fois toutes les 100 à 1000 millisecondes suffit largement pour un tableau de bord.

Node au lieu de Bun. Le cœur fonctionne aussi sur Node 20+, mais assurez-vous que le binaire natif précompilé correspond à votre plateforme ; réinstallez les dépendances si vous avez changé de runtime.

Prochaines étapes

Vous disposez maintenant du modèle mental de base : un renderer, un root React, une mise en page flexbox, des hooks clavier et la timeline. À partir d'ici, vous pouvez :

  • Ajouter un panneau de journal défilant avec le composant <scrollbox> pour la sortie en flux.
  • Rendre des extraits avec coloration syntaxique via la construction <code> avec un SyntaxStyle.
  • Extraire des widgets réutilisables dans une bibliothèque de composants interne, ou explorer les kits communautaires bâtis sur le cœur.
  • Remplacer le binding React par Solid si vous préférez une réactivité fine — le cœur est identique.

Pour approfondir, consultez nos guides connexes sur créer un outil CLI Node.js en TypeScript et l'agent de codage IA en terminal OpenCode, lui-même bâti sur OpenTUI.

Conclusion

OpenTUI apporte l'ergonomie déclarative et basée sur les composants du développement web moderne au terminal, soutenue par un cœur natif rapide. Dans ce tutoriel, vous avez construit un tableau de bord complet de monitoring système : vous avez configuré le renderer, disposé les panneaux avec flexbox, câblé l'état en direct avec les hooks React, géré la navigation clavier et animé une barre de progression avec la timeline. Les mêmes primitives passent d'un utilitaire interne rapide à des outils complets comme opencode. Le terminal est de nouveau une surface d'interface de première classe — et avec OpenTUI, développer pour lui ressemble à développer pour le web.