Skip to content

Latest commit

 

History

History
572 lines (420 loc) · 20.9 KB

File metadata and controls

572 lines (420 loc) · 20.9 KB

🔝 Retour au Sommaire

36.3.2 — Couleurs et styles

Section 36.3 : fmt — Formatage avancé (pré-C++23)


Introduction

Les couleurs dans un terminal ne sont pas de la décoration — elles sont un canal d'information. Un vert communique un succès instantanément, un texte rouge en gras signale une erreur critique sans que l'utilisateur ait besoin de lire le message. Les outils CLI professionnels — git, gcc, cargo, kubectl — utilisent tous la couleur comme vecteur sémantique.

Cette sous-section couvre l'API de colorisation de {fmt} (fmt/color.h) : couleurs de texte et de fond, styles d'emphase, composition, et surtout les patterns pratiques pour intégrer la couleur proprement dans un outil CLI — y compris la désactivation automatique quand la sortie n'est pas un terminal.


Les codes ANSI : ce qui se passe sous le capot

Avant d'utiliser l'API de {fmt}, il est utile de comprendre le mécanisme sous-jacent. Les terminaux modernes interprètent des séquences d'échappement ANSI pour modifier l'apparence du texte. Ces séquences commencent par \033[ (le caractère ESC suivi de [) et se terminent par une lettre de commande :

\033[31m    → texte rouge
\033[1m     → texte gras
\033[0m     → réinitialisation

On pourrait écrire directement ces codes :

// ✗ Ne faites pas ça — fragile, illisible, non portable
std::cout << "\033[1;31mERREUR\033[0m: fichier introuvable\n";

Ce code fonctionne, mais il est illisible, sujet aux erreurs (oublier le \033[0m de réinitialisation colore tout le reste du terminal), et ne gère pas le cas où la sortie est redirigée vers un fichier ou un pipe.

{fmt} encapsule ces séquences dans une API typée et composable, en s'assurant que la réinitialisation est toujours effectuée.


API de couleurs de {fmt}

Couleur de texte : fg()

#include <fmt/color.h>

fmt::print(fg(fmt::color::green), "✓ Succès\n");  
fmt::print(fg(fmt::color::red), "✗ Échec\n");  
fmt::print(fg(fmt::color::yellow), "⚠ Attention\n");  
fmt::print(fg(fmt::color::cyan), "ℹ Information\n");  

fg() (foreground) prend une valeur de l'énumération fmt::color et retourne un fmt::text_style. Les couleurs disponibles couvrent l'intégralité de la palette nommée CSS/X11 — plus de 140 couleurs. Les plus utiles pour un outil CLI :

Couleur Sémantique courante
fmt::color::green Succès, validation, état actif
fmt::color::red Erreur, échec, suppression
fmt::color::yellow Warning, attention, dépréciation
fmt::color::cyan Information, debug, métadonnées
fmt::color::white Texte principal, titres
fmt::color::gray Texte secondaire, désactivé
fmt::color::magenta Sections spéciales, identifiants
fmt::color::blue Liens, références, chemins

Couleur de fond : bg()

fmt::print(bg(fmt::color::red) | fg(fmt::color::white),
           " ERREUR CRITIQUE ");
fmt::print("\n");

fmt::print(bg(fmt::color::green) | fg(fmt::color::black),
           " DÉPLOYÉ ");
fmt::print("\n");

fmt::print(bg(fmt::color::yellow) | fg(fmt::color::black),
           " EN COURS ");
fmt::print("\n");

Les couleurs de fond sont utilisées avec parcimonie dans les outils CLI — principalement pour les badges de statut et les notifications critiques. Un abus de couleur de fond rend la sortie agressive et difficile à lire.

Styles d'emphase : fmt::emphasis

fmt::print(fmt::emphasis::bold, "Texte en gras\n");  
fmt::print(fmt::emphasis::italic, "Texte en italique\n");  
fmt::print(fmt::emphasis::underline, "Texte souligné\n");  
fmt::print(fmt::emphasis::strikethrough, "Texte barré\n");  

Les styles d'emphase disponibles :

Style Rendu Support terminal
bold Gras Universel
italic Italique Bon (la plupart des terminaux modernes)
underline Souligné Universel
strikethrough Barré Variable (pas tous les terminaux)
blink Clignotant Rare (souvent désactivé)

En pratique, bold est le seul style d'emphase utilisé couramment dans les outils CLI. underline sert parfois pour les URL ou les en-têtes de tableau. Les autres sont rarement pertinents.


Composition de styles

Les styles se composent avec l'opérateur | pour combiner couleurs et emphases :

// Rouge + gras : erreur critique
auto err_style = fg(fmt::color::red) | fmt::emphasis::bold;  
fmt::print(err_style, "FATAL: {}\n", message);  

// Cyan + italique : information secondaire
auto info_style = fg(fmt::color::cyan) | fmt::emphasis::italic;  
fmt::print(info_style, "Note: {}\n", hint);  

// Fond rouge + texte blanc + gras : badge d'erreur
auto badge_style = bg(fmt::color::red)
                 | fg(fmt::color::white)
                 | fmt::emphasis::bold;
fmt::print(badge_style, " {} ", "FAILED");  
fmt::print("\n");  

// Stocker un style pour réutilisation
auto header_style = fg(fmt::color::white) | fmt::emphasis::bold;  
auto dim_style = fg(fmt::color::gray);  
auto ok_style = fg(fmt::color::green) | fmt::emphasis::bold;  

L'opérateur | crée un nouveau fmt::text_style qui combine tous les attributs. L'ordre n'a pas d'importance : fg(...) | bold est identique à bold | fg(...).


Formatage avec couleurs et placeholders

fmt::print avec un style applique ce style à tout le texte formaté :

fmt::print(fg(fmt::color::red), "Erreur dans {} à la ligne {}\n",
           filename, line);
// Tout le message est en rouge, y compris le nom de fichier et le numéro

Pour colorer seulement une partie du texte, il faut séparer les appels ou utiliser fmt::format pour les parties non colorées :

// Pattern : préfixe coloré + message normal
fmt::print(fg(fmt::color::red) | fmt::emphasis::bold, "erreur");  
fmt::print(": impossible d'ouvrir '{}'\n", filepath);  
// "erreur" est en rouge gras, le reste est normal

// Pattern : label coloré + valeur
fmt::print(fg(fmt::color::gray), "  Durée : ");  
fmt::print("{:.2f}s\n", elapsed);  

Utiliser fmt::styled pour le formatage inline

Pour colorer un fragment à l'intérieur d'une format string, {fmt} offre fmt::styled :

fmt::print("Status: {}\n",
           fmt::styled("RUNNING", fg(fmt::color::green) | fmt::emphasis::bold));
// "Status: " est normal, "RUNNING" est vert gras

fmt::print("Compilation de {} — {}\n",
           fmt::styled(target, fg(fmt::color::cyan)),
           fmt::styled("OK", fg(fmt::color::green)));
// Le nom de la cible est en cyan, "OK" est en vert

fmt::styled est la manière idiomatique de mélanger texte coloré et texte normal dans un seul appel fmt::print. Elle prend une valeur et un style, et retourne un objet formattable qui s'insère dans un placeholder {}.


Palette sémantique pour un outil CLI

Plutôt que d'utiliser les couleurs directement dans le code métier, un outil bien architecturé définit une palette sémantique — un ensemble de styles nommés par intention, pas par couleur :

// styles.hpp — Palette sémantique
#pragma once
#include <fmt/color.h>

namespace cli::style {

// Niveaux de diagnostic
inline const auto success  = fg(fmt::color::green) | fmt::emphasis::bold;  
inline const auto error    = fg(fmt::color::red) | fmt::emphasis::bold;  
inline const auto warning  = fg(fmt::color::yellow);  
inline const auto info     = fg(fmt::color::cyan);  
inline const auto debug    = fg(fmt::color::gray);  

// Éléments d'interface
inline const auto heading  = fg(fmt::color::white) | fmt::emphasis::bold;  
inline const auto label    = fg(fmt::color::gray);  
inline const auto value    = fg(fmt::color::white);  
inline const auto path     = fg(fmt::color::blue) | fmt::emphasis::underline;  
inline const auto number   = fg(fmt::color::yellow);  
inline const auto keyword  = fg(fmt::color::magenta);  

// Badges (fond coloré)
inline const auto badge_ok   = bg(fmt::color::green) | fg(fmt::color::black);  
inline const auto badge_fail = bg(fmt::color::red) | fg(fmt::color::white)  
                             | fmt::emphasis::bold;
inline const auto badge_warn = bg(fmt::color::yellow) | fg(fmt::color::black);  
inline const auto badge_skip = bg(fmt::color::gray) | fg(fmt::color::white);  

}  // namespace cli::style

Utilisation :

#include "styles.hpp"

fmt::print(cli::style::heading, "=== Résultats des tests ===\n\n");

for (const auto& test : results) {
    if (test.passed) {
        fmt::print(cli::style::success, "");
    } else {
        fmt::print(cli::style::error, "");
    }
    fmt::print("{}", test.name);
    fmt::print(cli::style::label, " ({}ms)", test.duration_ms);
    fmt::print("\n");
}

fmt::print("\n");  
fmt::print(cli::style::badge_ok, " {} passés ", passed_count);  
fmt::print(" ");  
if (failed_count > 0) {  
    fmt::print(cli::style::badge_fail, " {} échoués ", failed_count);
} else {
    fmt::print(cli::style::debug, "0 échoués");
}
fmt::print("\n");

Résultat en terminal :

=== Résultats des tests ===

  ✓ test_parse_config (12ms)
  ✓ test_validate_input (3ms)
  ✗ test_network_timeout (5023ms)
  ✓ test_output_format (7ms)

 3 passés  1 échoués

Les avantages de cette approche par palette sémantique :

  • Cohérence : toutes les erreurs ont le même style partout dans l'outil.
  • Maintenabilité : changer la couleur des warnings se fait à un seul endroit.
  • Accessibilité : adapter la palette pour les daltoniens (voir plus loin) se fait sans toucher au code métier.
  • Testabilité : on peut remplacer la palette par des styles vides pour les tests.

Désactivation des couleurs : le pattern essentiel

Un outil CLI professionnel doit désactiver les couleurs quand la sortie n'est pas un terminal interactif. Les codes ANSI dans un fichier ou un pipe sont du bruit :

# Sans désactivation, un fichier de log contient des codes illisibles :
$ mon-outil > log.txt
$ cat log.txt
^[[1;32m✓^[[0m Build terminé    # Illisible

Détection de TTY

La détection se fait avec isatty() (POSIX) :

#include <unistd.h>

bool stdout_has_color = isatty(STDOUT_FILENO);  
bool stderr_has_color = isatty(STDERR_FILENO);  

Convention NO_COLOR et FORCE_COLOR

Au-delà de la détection TTY, il existe une convention communautaire (no-color.org) : si la variable d'environnement NO_COLOR est définie (quelle que soit sa valeur), les couleurs doivent être désactivées. Inversement, FORCE_COLOR force l'activation même en dehors d'un TTY.

#include <cstdlib>
#include <unistd.h>

enum class ColorMode { automatic, always, never };

ColorMode detect_color_mode() {
    // La variable d'environnement NO_COLOR est prioritaire
    if (std::getenv("NO_COLOR") != nullptr)
        return ColorMode::never;

    // FORCE_COLOR force l'activation
    if (std::getenv("FORCE_COLOR") != nullptr)
        return ColorMode::always;

    return ColorMode::automatic;
}

bool should_colorize(int fd, ColorMode mode) {
    switch (mode) {
        case ColorMode::always: return true;
        case ColorMode::never:  return false;
        case ColorMode::automatic:
        default: return isatty(fd);
    }
}

Intégration avec CLI11

L'option --color / --no-color que nous avons vue en section 36.1.2 s'intègre naturellement :

// Parsing
bool use_color = true;  
app.add_flag("--color,!--no-color", use_color, "Activer/désactiver les couleurs");  

CLI11_PARSE(app, argc, argv);

// Résolution finale : CLI > env > TTY
ColorMode env_mode = detect_color_mode();  
bool colorize_stdout, colorize_stderr;  

if (app.get_option("--color")->count() > 0) {
    // L'utilisateur a explicitement passé --color ou --no-color
    colorize_stdout = colorize_stderr = use_color;
} else {
    // Pas d'option explicite : utiliser env + TTY
    colorize_stdout = should_colorize(STDOUT_FILENO, env_mode);
    colorize_stderr = should_colorize(STDERR_FILENO, env_mode);
}

Pattern d'impression conditionnelle

Une fois la décision prise, il faut un mécanisme pour appliquer ou ignorer les styles. Le plus simple est une fonction helper :

// Retourne le style si la couleur est active, un style vide sinon
inline fmt::text_style maybe(fmt::text_style style, bool colorize) {
    return colorize ? style : fmt::text_style{};
}

Utilisation :

fmt::print(maybe(cli::style::success, colorize_stderr), "✓ ");  
fmt::print(stderr, "Build terminé\n");  

Un fmt::text_style{} par défaut (vide) n'émet aucun code ANSI — le texte est affiché normalement. Ce pattern est léger et non intrusif.

Approche alternative : wrapper centralisé

Pour les projets plus importants, la section 36.3 a présenté un module output.hpp qui encapsule la détection TTY et les styles. Cette approche est préférable car elle centralise la logique de colorisation :

namespace cli {

class Output {
    bool colorize_stdout_;
    bool colorize_stderr_;

public:
    Output(bool color_stdout, bool color_stderr)
        : colorize_stdout_(color_stdout)
        , colorize_stderr_(color_stderr) {}

    template <typename... Args>
    void success(fmt::format_string<Args...> fmts, Args&&... args) {
        auto s = colorize_stderr_ ? style::success : fmt::text_style{};
        fmt::print(stderr, s, "");
        fmt::print(stderr, fmts, std::forward<Args>(args)...);
        fmt::print(stderr, "\n");
    }

    template <typename... Args>
    void error(fmt::format_string<Args...> fmts, Args&&... args) {
        auto s = colorize_stderr_ ? style::error : fmt::text_style{};
        fmt::print(stderr, s, "");
        fmt::print(stderr, fmts, std::forward<Args>(args)...);
        fmt::print(stderr, "\n");
    }

    // warn(), info(), debug() suivent le même pattern...

    // Données sur stdout (pour les pipes)
    template <typename... Args>
    void data(fmt::format_string<Args...> fmts, Args&&... args) {
        fmt::print(fmts, std::forward<Args>(args)...);
    }
};

}  // namespace cli
// main.cpp
cli::Output out(colorize_stdout, colorize_stderr);

out.success("Compilation terminée en {:.1f}s", elapsed);  
out.error("Fichier introuvable : {}", path);  
out.data("{}\n", json_output);  // Données brutes sur stdout  

Ce pattern sépare clairement les sorties diagnostiques (stderr, potentiellement colorées) des sorties de données (stdout, jamais colorées, exploitables en pipe).


Tableaux colorés

Les tableaux sont un format de sortie récurrent dans les outils CLI. Combiner {fmt} pour l'alignement et les couleurs produit des résultats professionnels :

void print_service_table(const std::vector<Service>& services,
                         bool colorize) {
    auto h = [&](fmt::text_style s) {
        return colorize ? s : fmt::text_style{};
    };

    // En-tête
    fmt::print(h(style::heading),
               "{:<20} {:>6} {:>10} {:>12}\n",
               "SERVICE", "PORT", "STATUS", "UPTIME");
    fmt::print(h(style::label),
               "{:─<20} {:─>6} {:─>10} {:─>12}\n",
               "", "", "", "");

    // Lignes
    for (const auto& svc : services) {
        // Nom
        fmt::print(h(style::value), "{:<20} ", svc.name);

        // Port
        fmt::print(h(style::number), "{:>6} ", svc.port);

        // Status (couleur conditionnelle)
        auto status_style = svc.running ? style::success : style::error;
        std::string status_text = svc.running ? "running" : "stopped";
        fmt::print(h(status_style), "{:>10} ", status_text);

        // Uptime
        if (svc.running) {
            fmt::print(h(style::label), "{:>12}\n", svc.uptime_str());
        } else {
            fmt::print(h(style::debug), "{:>12}\n", "");
        }
    }
}

Résultat en terminal (avec couleurs) :

SERVICE              PORT     STATUS       UPTIME
──────────────────── ────── ────────── ────────────
nginx                  80    running       7d 4h  
api                  3000    running      12h 30m  
worker               9090    stopped            —  
postgres             5432    running       7d 4h  

Le même code, quand la sortie est redirigée (> fichier.txt), produit un tableau identique mais sans codes ANSI — parfaitement lisible dans un fichier texte.


Barres de progression

Une barre de progression est un élément d'interface courant pour les opérations longues. On la construit avec un retour chariot (\r) pour réécrire la même ligne :

void print_progress(const std::string& label, int current, int total,
                    bool colorize) {
    int pct = (total > 0) ? (100 * current / total) : 0;
    int bar_width = 30;
    int filled = bar_width * current / std::max(total, 1);

    std::string bar(filled, '#');
    std::string empty(bar_width - filled, '.');

    if (colorize) {
        fmt::print(stderr, "\r{} [", label);
        fmt::print(stderr, fg(fmt::color::green), "{}", bar);
        fmt::print(stderr, fg(fmt::color::gray), "{}", empty);
        fmt::print(stderr, "] {:>3}%", pct);
    } else {
        fmt::print(stderr, "\r{} [{}{}] {:>3}%", label, bar, empty, pct);
    }

    if (current >= total) {
        fmt::print(stderr, "\n");  // Terminer la ligne quand c'est fini
    }
    std::fflush(stderr);  // Forcer l'affichage immédiat
}
// Utilisation
for (int i = 0; i <= file_count; ++i) {
    process_file(files[i]);
    print_progress("Traitement", i, file_count, colorize_stderr);
}

Résultat animé dans le terminal :

Traitement [################..............] 53%

Points importants :

  • \r (retour chariot) : ramène le curseur au début de la ligne sans saut de ligne, permettant de réécrire la barre à chaque itération.
  • std::fflush(stderr) : force l'écriture immédiate. Sans cela, le buffer de sortie peut retarder l'affichage et la barre apparaît par à-coups.
  • Écriture sur stderr : la barre de progression est un élément de diagnostic, pas une donnée. Elle va sur stderr pour ne pas polluer un pipe sur stdout.
  • Saut de ligne final : quand la progression atteint 100%, un \n est émis pour ne pas écraser la dernière ligne.

💡 Pour des barres de progression plus sophistiquées (multi-barres, estimation du temps restant, débit), des librairies spécialisées comme indicators existent. Mais pour un outil CLI standard, le pattern ci-dessus couvre la majorité des besoins.


Considérations d'accessibilité

Daltonisme

Environ 8% des hommes ont une forme de daltonisme. La forme la plus courante (deutéranopie/protanopie) rend difficile la distinction rouge/vert — précisément les deux couleurs les plus utilisées pour succès/erreur. Quelques mesures simples :

  • Ne jamais se reposer uniquement sur la couleur. Utilisez aussi des symboles (/), des mots (OK/ERREUR), ou du gras. L'information doit être accessible sans couleur.
  • Préférer des contrastes de luminosité plutôt que des contrastes de teinte seule.
  • Tester en monochrome : si votre sortie est lisible sans couleur, elle est accessible.

La palette sémantique définie plus haut respecte ces principes — chaque niveau de diagnostic a un symbole distinctif en plus de la couleur.

Terminaux à thème clair

Les couleurs qui fonctionnent sur fond sombre peuvent être illisibles sur fond clair (et inversement). En 2026, la plupart des terminaux sont en thème sombre, mais ce n'est pas universel. Le blanc comme couleur de texte principal est un piège — il est invisible sur fond blanc.

La recommandation est de ne pas colorer le texte courant et de n'utiliser la couleur que pour les accents (préfixes, badges, valeurs remarquables). Le texte principal hérite de la couleur par défaut du terminal, qui est toujours lisible quel que soit le thème.


Récapitulatif des patterns

Pattern Quand l'utiliser
fg(color) seul Messages de statut (succès, erreur, warning)
fg(color) | bold Titres, en-têtes, messages critiques
bg(color) | fg(color) Badges de statut (usage modéré)
fmt::styled(val, style) Fragment coloré dans un message mixte
Palette sémantique Tout projet au-delà du prototype
maybe(style, colorize) Désactivation conditionnelle légère
Classe Output Outil CLI de taille moyenne à grande
\r + barre Opérations longues avec progression
Symboles + couleur Accessibilité (jamais la couleur seule)

Cette section conclut la couverture de {fmt}. Combinée avec CLI11 pour le parsing (36.1) et les conventions de sortie vues dans l'introduction du chapitre, vous disposez des outils pour construire des sorties CLI professionnelles. La section suivante (36.4) approfondit la gestion du terminal — détection TTY, dimensions du terminal, et adaptation automatique du comportement.

⏭️ Gestion des couleurs et du TTY