Colorer la sortie standard avec des manipulateurs de flux

Les manipulateurs de flux — stream manipulators —, en plus de modifier la manière dont les sorties sont affichées, peuvent être employés afin de manipuler les attributs d'un terminal. En particulier, il est possible de modifier la couleur d'impression des caractères.

Linux

Sous Linux, pour changer la couleurs des caractères affichés, il s'agit simplement d'insérer un code de contrôle dans le flux de sortie. La liste des codes de contrôle peut être consultée ici. Par exemple, pour afficher le texte en rouge, le manipulateur de flux suivant peut être défini. En pratique, celui-ci ne fait qu'insérer le code de contrôle \033[31m dans le flux de sortie.

template<typename Char> inline std::basic_ostream<Char>&
red(std::basic_ostream<Char>& out) {
    return out << "\033[31m";
}

Puisque chaque manipulateur est similaire, une macro simplifie le travail de définition des manipulateurs de flux subséquents.

#define TTY_MANIPULATOR(name, code) \
template<typename Char> \
inline std::basic_ostream<Char>& \
name(std::basic_ostream<Char>& out) { \
    return out << "\033["#code"m"; \
}

Les manipulateurs de flux peuvent alors être déclarés de la manière suivante.

TTY_MANIPULATOR(reset, 0)
TTY_MANIPULATOR(black, 30)
TTY_MANIPULATOR(red, 31)
TTY_MANIPULATOR(green, 32)
TTY_MANIPULATOR(yellow, 33)
TTY_MANIPULATOR(blue, 34)
TTY_MANIPULATOR(magenta, 35)
TTY_MANIPULATOR(cyan, 36)
TTY_MANIPULATOR(white, 37)
TTY_MANIPULATOR(bold, 1)

Windows

Sous Windows, la tâche est un peu plus complexe[1]. Dans un premier temps, il importe de sauvegarder les attributs initiaux du terminal. Pour ce faire, l'approche la plus simple consiste à définir une variable statique initialisée au démarrage du programme.

WORD SaveOriginalConsoleAttrs() {
    HANDLE stdout_handle = GetStdHandle(STD_OUTPUT_HANDLE);
    if (!stdout_handle || stdout_handle == INVALID_HANDLE_VALUE)
        return 0;
    CONSOLE_SCREEN_BUFFER_INFO buffer_info;
    GetConsoleScreenBufferInfo(stdout_handle, &buffer_info);
    return buffer_info.wAttributes;
}
const static WORD RESET_ATTRS = SaveOriginalConsoleAttrs();

Contrairement à Linux où un code unique est associé à chaque couleur, la console de Windows compose ses couleurs à partir de rouge, vert et bleu. Aussi, avant de modifier la couleur du terminal, il est nécessaire de vidanger le tampon du flux sans quoi les caractères non affichés seront également colorés. Par exemple, pour afficher le texte en rouge, le manipulateur de flux suivant peut être défini. Tout comme lors de la sauvegarde des paramètres initiaux, les erreurs sont tout simplement ignorés silencieusement.

template<typename Char> std::basic_ostream<Char>&
red(std::basic_ostream<Char>& out) {
    HANDLE stdout_handle = GetStdHandle(STD_OUTPUT_HANDLE);
    if (stdout_handle && stdout_handle != INVALID_HANDLE_VALUE) {
        out.flush();
        SetConsoleTextAttribute(stdout_handle, FOREGROUND_RED | FOREGROUND_INTENSITY);
    }
    return out;
}

Encore une fois, puisque chaque fonction est similaire, une macro peut être définie afin de simplifier la définition des manipulateurs subséquents.

#define COLOR_MANIPULATOR(name, color) \
template<typename Char> \
inline std::basic_ostream<Char>& \
name(std::basic_ostream<Char>& out) { \
    HANDLE stdout_handle = GetStdHandle(STD_OUTPUT_HANDLE); \
    if (stdout_handle && stdout_handle != INVALID_HANDLE_VALUE) { \
        out.flush(); \
        SetConsoleTextAttribute(stdout_handle, color); \
    } \
    return out; \
}

Les manipulateurs de flux peuvent alors être déclarés de la manière suivante. Tel que mentionné précédemment, la variable statique RESET_ATTRS contient les attributs initiaux du terminal.

COLOR_MANIPULATOR(reset, details::RESET_ATTRS)
COLOR_MANIPULATOR(black, 0)
COLOR_MANIPULATOR(red, FOREGROUND_RED | FOREGROUND_INTENSITY)
COLOR_MANIPULATOR(green, FOREGROUND_GREEN | FOREGROUND_INTENSITY)
COLOR_MANIPULATOR(yellow, FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_INTENSITY)
COLOR_MANIPULATOR(blue, FOREGROUND_BLUE | FOREGROUND_INTENSITY)
COLOR_MANIPULATOR(magenta, FOREGROUND_RED | FOREGROUND_BLUE | FOREGROUND_INTENSITY )
COLOR_MANIPULATOR(cyan, FOREGROUND_GREEN | FOREGROUND_BLUE | FOREGROUND_INTENSITY)
COLOR_MANIPULATOR(white, FOREGROUND_GREEN | FOREGROUND_RED | FOREGROUND_BLUE | FOREGROUND_INTENSITY)

Conclusion

Ces manipulateurs de flux étant maintenant disponibles, colorer la sortie standard se révèle être d'une simplicité désarmante. D'ailleurs, la syntaxe est pour le moins naturelle.

#include <iostream>
#include "ttym.hpp"

using namespace std;
using namespace ttym;

int main() {

    cout << black << "black" << endl;
    cout << red << "red" << endl;
    cout << blue << "blue" << endl;
    cout << green << "green" << endl;
    cout << white << "white" << endl;
    cout << cyan << "cyan" << endl;
    cout << yellow << "yellow" << endl;
    cout << magenta << "magenta" << endl;

    cout << bold;

    cout << black << "black" << endl;
    cout << red << "red" << endl;
    cout << blue << "blue" << endl;
    cout << green << "green" << endl;
    cout << white << "white" << endl;
    cout << cyan << "cyan" << endl;
    cout << yellow << "yellow" << endl;
    cout << magenta << "magenta" << endl;

    cout << reset;

    return 0;
}

Le code source complet est disponible en ligne — caveat emptor.

  1. En sommes-nous vraiment étonnés ?