🔝 Retour au Sommaire
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.
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.
#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 |
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.
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 |
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.
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(...).
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éroPour 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); 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 vertfmt::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 {}.
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::styleUtilisation :
#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.
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é # IllisibleLa détection se fait avec isatty() (POSIX) :
#include <unistd.h>
bool stdout_has_color = isatty(STDOUT_FILENO);
bool stderr_has_color = isatty(STDERR_FILENO); 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);
}
}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);
}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.
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).
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.
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 surstderrpour ne pas polluer un pipe surstdout. - Saut de ligne final : quand la progression atteint 100%, un
\nest é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.
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.
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.
| 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.