Skip to content

RawTerminal

Harry Broeders edited this page Nov 30, 2023 · 2 revisions

Terminalprogramma in raw mode met kleur

Een C-programma dat in een terminal draait, werkt standaard in de zogenoemde cooked mode. In deze mode wordt de input pas doorgestuurd naar het programma als de gebruiker op enter heeft gedrukt. Dit geeft de gebruiker de kans om de input nog te corrigeren (bijvoorbeeld door Backspace te gebruiken) voordat het verwerkt wordt door het programma. In de meeste gevallen is dit handig, behalve als je wilt dat je programma meteen reageert zodra de gebruiker een toets indrukt. Om dit mogelijk te maken, moet je de terminal in de zogenoemde raw mode gebruiken. Hoe je dit doet is afhankelijk van het platform en het type terminal dat je gebruikt.

Als je in een C-programma dat in een terminal draait kleur wilt gebruiken, dan kun je gebruik maken van zogenoemde ANSI/VT100 Terminal Control Escape Sequences. Een overzicht vind je hier: ANSI/VT100 Terminal Control. Ook als je bijvoorbeeld het indrukken van de pijltjestoetsen in je programma wilt afvangen, dan kun je deze ANSI Escape Sequences gebruiken. In de onderstaande programma's is gebruik gemaakt van deze codes om de hexadecimale ASCII-code van een ingetypt karakter in het rood weer te geven.

Bijna alle terminalemulatieprogramma's ondersteunen deze ANSI/VT100 Escape Sequences (bijvoorbeeld PuTTY en TeraTerm). CoolTerm ondersteunt deze codes echter niet. Ook de ingebouwde terminal (Console) in Eclipse ondersteunt deze codes niet.

[TOC]

Visual Studio Code terminal met WSL

Om de Visual Studio Code terminal onder WSL in raw mode te kunnen gebruiken maken we gebruik van de termio library termio. In het onderstaande programma maken we gebruik van de functies tcgetattr() en tcsetattr om de terminal in het begin van het programma in raw mode en aan het einde van het programma weer in cooked mode te zetten. We kunnen dan de standaardfunctie getchar() gebruiken om een ingetypt karakter meteen in te lezen.

#include <stdio.h>
#include <stdbool.h>
#include <termios.h>

#define ANSI_COLOR_RED "\x1b[31m"
#define ANSI_COLOR_RESET "\x1b[0m"

static struct termios term;

static bool set_terminal_in_VT_mode(void)
{
    if (tcgetattr(0, &term) != 0) return false;
    struct termios raw = term;
    raw.c_lflag &= ~ICANON; // disable buffered I/O
    raw.c_lflag &= ~ECHO; // disable echo mode
    return tcsetattr(0, TCSANOW, &raw) == 0;
}

static bool restore_terminal_mode(void)
{
    return tcsetattr(0, TCSANOW, &term) == 0;
}

int main()
{
    if (!set_terminal_in_VT_mode())
    {
        perror(NULL);
        return -1;
    }
    
    puts("Type charakters (de ASCII code van elk karakter wordt voor het karakter in het rood geprint) (0 om te stoppen):");

    char c;
    do
    {
        c = getchar();
        printf(ANSI_COLOR_RED "0x%02x" ANSI_COLOR_RESET, c);
        putchar(c);
    } while (c != '0');

    if (!restore_terminal_mode())
    {
        perror(NULL);
        return -1;
    }

    return 0;
}

Het intypen van Hallo<Enter>Wereld<Enter>0 levert de volgende output op:

Terminal met gcc in Linux

Het bovenstaande programma werkt ook prima in een Linux terminal. Het intypen van Hallo<Enter>Wereld!<Enter>0 levert de volgende output op:

Eclipse met MinGW64 in Windows

Windows beschikt over specifieke system calls om een terminal ANSI/VT100 mode te gebruiken: The Console API. In het onderstaande programma wordt de functie ReadConsoleA() gebruikt om een ingetypt karakter meteen in te lezen.

#include <stdio.h>
#include <stdbool.h>
#include <windows.h>

#define ANSI_COLOR_RED "\x1b[31m"
#define ANSI_COLOR_RESET "\x1b[0m"

static HANDLE hOut = NULL;
static HANDLE hIn = NULL;
static DWORD dwOriginalOutMode = 0;
static DWORD dwOriginalInMode = 0;

// Own implementation of getchar to circumvent known error in standard getchar https://github.com/microsoft/terminal/issues/8820
int getchar()
{
    char c;
    DWORD numRead;
    if (!ReadConsoleA(hIn, &c, 1, &numRead, NULL) || numRead < 1)
    {
        return EOF;
    }
    return (int)c;
}

static bool set_terminal_in_VT_mode(void)
{
    // Set output mode to handle virtual terminal sequences
    hOut = GetStdHandle(STD_OUTPUT_HANDLE);
    if (hOut == INVALID_HANDLE_VALUE)
    {
        return false;
    }
    hIn = GetStdHandle(STD_INPUT_HANDLE);
    if (hIn == INVALID_HANDLE_VALUE)
    {
        return false;
    }

    if (!GetConsoleMode(hOut, &dwOriginalOutMode))
    {
        return false;
    }
    if (!GetConsoleMode(hIn, &dwOriginalInMode))
    {
        return false;
    }

    if (!SetConsoleMode(hOut, ENABLE_VIRTUAL_TERMINAL_PROCESSING | ENABLE_PROCESSED_OUTPUT))
    {
        // Failed to set any VT mode, can't do anything here.
        return false;
    }

    if (!SetConsoleMode(hIn, ENABLE_VIRTUAL_TERMINAL_INPUT /*| ENABLE_PROCESSED_INPUT*/ ))
    {
        // Failed to set VT input mode, can't do anything here.
        return false;
    }
    return true;
}

static bool restore_terminal_mode(void)
{
    if (!SetConsoleMode(hOut, dwOriginalOutMode))
    {
        return false;
    }

    if (!SetConsoleMode(hIn, dwOriginalInMode))
    {
        return false;
    }
    return true;
}

int main()
{
    if (!set_terminal_in_VT_mode())
    {
        return -1;
    }

    puts("Type charakters (de ASCII code van elk karakter wordt voor het karakter in het rood geprint) (0 om te stoppen):");

    char c;
    do
    {
        c = getchar();
        printf(ANSI_COLOR_RED "0x%02x" ANSI_COLOR_RESET, c);
        if (c == '\r')
        {
            putchar('\n'); // extra linefeed bij carriage return
        }
        putchar(c);
    } while (c != '0');

    if (!restore_terminal_mode())
    {
        return -1;
    }

    return 0;
}

Om een terminal in raw mode te gebruiken in Eclipse, moeten je het programma m.b.v. de debugger uitvoeren. Omdat de ingebouwde console niet goed werkt moet je daarbij bovendien gebruik maken van een externe console.

Kies in het menu: Run > Debug Configurations... en selecteer vervolgens het tabblad Debugger. Zet een vinkje bij Use external console for inferior (open a new console window for input/output):

Nu verschijnt er een apart console window als de debugger wordt gestart, waarin de output verschijnt en de input ingetypt kan worden.

Het intypen van Hallo<Enter>Wereld<Enter> levert de volgende output op:

In Windows 11 kun je in plaats van het externe console kun je ook een externe terminal gebruiken. Om dit in te stellen klik je met je rechtermuisknop op de titelbalk van het window:

Kies voor Eigenschappen:

Selecteer Windows Terminal en start de debugger opnieuw.

Het intypen van Hallo<Enter>Wereld<Enter> levert nu de volgende output op:

Visual Studio Code met MinGW64 in Windows

Als je het bovenstaande programma in VS Code runt of debugt, dan verschijnt de (correcte) uitvoer in het ingebouwde TERMINAL window van VS Code:

MSYS2 terminal met MinGW64 in Windows

Om de MSYS2 terminal in raw mode te kunnen gebruiken maken we gebruik van het commando stty. In het onderstaande programma maken we gebruik van de functie system() om de terminal in het begin van het programma in raw mode en aan het einde van het programma weer in cooked mode te zetten. We kunnen dan de standaardfunctie getchar() gebruiken om een ingetypt karakter meteen in te lezen.

#include <stdio.h>
#include <stdbool.h>
#include <stdlib.h>

#define ANSI_COLOR_RED "\x1b[31m"
#define ANSI_COLOR_RESET "\x1b[0m"

static bool set_terminal_in_VT_mode(void)
{
       if (setvbuf(stdout, NULL, _IONBF, 0))
       {
           return false;
       }
    if (system("stty -icanon"))
    {
        return false;
    }
    if (system("stty -echo"))
    {
        return false;
    }
    return true;
}

static bool restore_terminal_mode(void)
{
    if (system("stty icanon"))
    {
        return false;
    }
    if (system("stty echo"))
    {
        return false;
    }
    return true;
}

int main()
{
    if (!set_terminal_in_VT_mode())
    {
        return -1;
    }

    puts("Type charakters (de ASCII code van elk karakter wordt voor het karakter in het rood geprint) (0 om te stoppen):");

    char c;
    do
    {
        c = getchar();
        printf(ANSI_COLOR_RED "0x%02x" ANSI_COLOR_RESET, c);
        putchar(c);
    } while (c != '0');

    if (!restore_terminal_mode())
    {
        return -1;
    }

    return 0;
}

Het intypen van Hallo<Enter>Wereld<Enter> levert de volgende output op:

Clone this wiki locally