Skip to content

Instantly share code, notes, and snippets.

@Nek-
Last active August 8, 2022 15:48
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Nek-/119149c2b662ff9580e5d27261f83aef to your computer and use it in GitHub Desktop.
Save Nek-/119149c2b662ff9580e5d27261f83aef to your computer and use it in GitHub Desktop.
Quelques fonctions pour utiliser le terminal en PHP
<?php declare(strict_types=1);
// Ce fichier contient tout un tas de fonctions utile pour faire des opérations spéciales
// dans les terminaux.
// Lire le contenu des fonctions n'est pas toujours simple car on utilise justement des fonctions pour se simplifier
// la vie, leur contenu est souvent donc un peu compliqué à lire.
// Mais ici pour les curieux j'ai pris le temps de commenter le code !
// Pour en apprendre plus comment cela fonctionne vous pouvez aller lire ce tutoriel sur ZdS:
// https://zestedesavoir.com/tutoriels/1733/termcap-et-terminfo/
// Ou naturellement faire quelques recherches sur internet. ;-) (c'est d'ailleurs principalement ce que j'ai fait)
// Tout d'abord je commence par définir quelques couleurs. La fonction define permet de définir des constantes globales.
// Ce sont des valeurs réutilisables, mais leur valeur n'est pas modifiables, on les appelle sans le `$`, par exemple:
// $couleur = BLANC;
// var_dump($couleur); // affichera int(1)
define('BLANC', 97);
define('NOIR', 30);
define('BLEU', 34);
define('ROUGE', 31);
define('VERT', 32);
define('JAUNE', 33);
define('FOND_BLANC', 107);
define('FOND_NOIR', 40);
define('FOND_BLEU', 44);
define('FOND_ROUGE', 41);
define('FOND_VERT', 42);
define('FOND_JAUNE', 43);
define('CORRESPONDANCE_HAUT', 'z');
define('CORRESPONDANCE_BAS', 's');
define('CORRESPONDANCE_GAUCHE', 'q');
define('CORRESPONDANCE_DROITE', 'd');
define('HAUT', 1);
define('BAS', 2);
define('GAUCHE', 3);
define('DROITE', 4);
// Cette instruction particulière permet d'effectuer une action à l'appui des touches CTRL+C
declare(ticks = 1);
/**
* Cette fonction permet de cacher le curseur du terminal.
*
* Elle écrit sur le stream STDOUT des caractères spéciaux qui vont informer le terminal
* qu'on veut effacer le curseur. Elle déclare aussi une fonction qui s'exécutera à la fin
* de l'exécution, son but : rétablir le curseur ! Toujours avec des caractères spéciaux.
*/
function cacherCurseur(): void
{
fprintf(STDOUT, "\033[?25l"); // cache le curseur
// A la fin de l'exécution on affiche le curseur
register_shutdown_function(function() {
fprintf(STDOUT, "\033[?25h"); // montre le curseur
});
// Si jamais on fait CTRL+C on réaffiche le curseur (et on arrête le programme)
if (function_exists('pcntl_signal')) {
// Cette fonction n'existe que sous linux et macos
pcntl_signal(SIGINT, function () {
fprintf(STDOUT, "\033[?25h"); // montre le curseur
exit;
});
} else {
// Sous windows on doit en utiliser une autre.
sapi_windows_set_ctrl_handler(function (int $event) {
if ($event === PHP_WINDOWS_EVENT_CTRL_C) {
fprintf(STDOUT, "\033[?25h"); // montre le curseur
exit;
}
});
}
}
/**
* @param int $x Coordonnée X dans notre terminal
* @param int $y Coordonnée Y dans notre terminal
* @param string $text Texte à afficher aux coordonnées données
* @param int|null $color Couleur du texte (disponible en constante en haut du fichier)
*/
function afficherTexte(int $x, int $y, string $text, ?int $color = null): void
{
// Je vérifie que la couleur est supportée par notre fonction :
if ($color !== null && !in_array($color, [BLANC, NOIR, BLEU, ROUGE, VERT, JAUNE, FOND_BLANC, FOND_NOIR, FOND_BLEU, FOND_ROUGE, FOND_VERT, FOND_JAUNE])) {
throw new \Exception("La couleur '$color' n'est pas supportée, utilisez la liste des constantes de couleurs.");
// Remarque: cette étape est optionnelle, mais c'est sur la vérification de ce genre de choses que la différence est faite
// entre un bon et un mauvais code.
// Ici, en utilisant cette fonction de la mauvaise façon, vous aurez tout de suite une erreur claire: c'est plutôt un bon code.
// Typiquement sans cette vérification la suite peut être... Surprenante.
}
// Il vaut mieux éviter d'appeler trop souvent la fonction tailleFenetre() (voir les commentaires de la fonction)
// pour le faire ici on utilise des variables statiques.
// Ces variables une fois définies le resteront même au prochain appel de la fonction !
static $xMax;
static $yMax;
if ($xMax === null || $yMax === null) {
[$xMax, $yMax] = tailleFenetre();
}
// Je vérifie que les coordonnées entrées soient valide
// Remarque: un code dit "faible" n'aurait pas effectué cette vérification, mais vous auriez eu
// beaucoup plus de mal à trouver le problème en cas d'erreur !
if ($x < 0 || $y < 0 || $x > $xMax || $y > $yMax) {
throw new \LogicException("Les coordonnées données ($x, $y) sont en dehors du terminal, le maximum est ($xMax, $yMax) !");
}
// Si la couleur est spécifiée, alors on transforme le texte dans la couleur désirée.
if (null !== $color) {
// C'est la syntaxe pour afficher une couleur sur le terminal.
$text = "\033[".$color.'m' . $text . "\033[0m";
}
fprintf(STDOUT,"\x1b7\x1b[".$y.';'.$x.'f'.$text."\x1b8");
}
/**
* La fonction tailleFenetre fait appel à un autre programme qui écrit la taille de la fenêtre (deux chiffres)
* sur la sortie standard.
*
* Elle s'occupe ensuite de découper ces deux chiffres à l'aide de la fonction explode.
*
* Attention tout de même, l'appel d'un programme externe peut se révéler coûteux pour PHP car on "sort" de PHP. De
* manière générale on va éviter les appels externe autant qu'on le peut, de plus cela peut aussi poser des problèmes
* de sécurité.
*
* @return array
*/
function tailleFenetre(): array
{
// La syntaxe `` est un peu particulière en PHP. C'est une façon rapide d'utiliser la fonction "exec"
// pour exécuter un programme externe. On l'utilise rarement principalement parce qu'on ne
// contrôle pas grand chose avec son utilisation et elle peut même s'avérer dangereuse dans certains cas.
// Mais pour notre petit exercice cela conviendra parfaitement.
if (substr(PHP_OS, 0, 3) == "WIN") {
// La version pour windows
$info = `mode CON`;
if (null === $info || !preg_match('/--------+\r?\n.+?(\d+)\r?\n.+?(\d+)\r?\n/', $info, $matches)) {
return null;
}
return [(int) $matches[2], (int) $matches[1]];
}
// C'est quand même plus simple sous linux hein ;) .
$width = intval(`tput cols`);
$height = intval(`tput lines`);
return [$width, $height];
}
function effacer(): void
{
// C'est les caractères qui font comprendre au terminal qu'il faut tout effacer !
fprintf(STDOUT,"\033[H\033[J");
}
/**
* Comme je l'ai mentionné en introduction, il y a une différence énorme entre Windows et les autres
* pour arriver à faire tout cela. Cette fonction a donc comme but principal de détecter windows et
* appeler la fonction correspondante, la fonction pour les systèmes "unix" sinon.
*/
function toucheAppuyee(): ?int
{
// Codes des touches qui fonctionneraient sous linux
// Touche HAUT: 27 91 65
// Touche BAS: 27 91 66
// Touche GAUCHE: 27 91 68
// Touche DROITE: 27 91 67
$touches = [
HAUT => CORRESPONDANCE_HAUT,// chr(27) . chr(91) . chr(65),
BAS => CORRESPONDANCE_BAS, // chr(27) . chr(91) . chr(66),
GAUCHE => CORRESPONDANCE_GAUCHE, // chr(27) . chr(91) . chr(68),
DROITE => CORRESPONDANCE_DROITE, // chr(27) . chr(91) . chr(67),
];
if (DIRECTORY_SEPARATOR === '\\') {
// Dans le cas de windows
return windowsToucheAppuyee($touches);
}
// Sinon dans le cas général (vous ne rêvez pas, c'est bien un "sinon" sans else, car je return dans le if)
return unixToucheAppuyee($touches);
}
/**
* Ce qui se passe dans cette fonction est un peu complexe, je l'ai commenté pour les plus curieux mais elle fait
* appel à une notion avancée: les streams. Je ne détaille cela que dans la partie 3 du cours ! J'utilise ici ce
* méchanisme car nous faisons quelque chose d'assez inhabituel: un jeu dans le terminal.
*
* Sachez que comprendre cela n'est pas vraiment important pour déjà bien exploiter PHP. Et d'ailleurs la preuve est
* sous vos yeux: vous pouvez utiliser cette fonction sans même savoir ce qu'est un stream, et dans la plupart des cas
* on va utiliser des fonctions pour nous simplifier la vie sur les sujets complexes.
*
* @param int[] $touches Les touches à détecter
*
* @return int|null Une des valueur de constante TOUCHE_HAUT, TOUCHE_BAS, TOUCHE_GAUCHE, TOUCHE_DROITE
*/
function unixToucheAppuyee(array $touches): ?int
{
static $entreePrete = false;
if (!$entreePrete) {
// On utilise une variable statique car on ne veut exécuter cette fonction qu'une seule fois.
stream_set_blocking(STDIN, false);
$entreePrete = true;
}
readline_callback_handler_install('', function () {});
$caractere = stream_get_contents(STDIN);
readline_callback_handler_remove();
foreach ($touches as $touche => $valeurTouche) {
if ($valeurTouche === $caractere) {
return $touche;
}
}
return null;
}
// J'utilise ces constantes pour y voir plus clair dans la suite du code qui est déjà coriace.
define('STD_INPUT_HANDLE', -10);
// https://docs.microsoft.com/fr-fr/windows/console/setconsolemode
define('ENABLE_ECHO_INPUT', 0x0004);
define('ENABLE_PROCESSED_INPUT', 0x0001);
define('ENABLE_WINDOW_INPUT', 0x0008);
// https://docs.microsoft.com/fr-fr/windows/console/input-record-str
define('KEY_EVENT', 0x0001);
/**
* Chers petits curieux, je pense que vous pouvez passer votre chemin ici !
* Le principe de réutiliser des fonctions est de pouvoir utiliser "facilement" du code bien plus compliqué. Vous ne
* pouviez pas tomber sur un plus bel exemple. Le code qui suit est très complexe, j'ai même eu du mal à l'écrire.
* J'ai maudit plusieurs fois Windows mais je tenais à ce que ce TP fonctionne bien même sous Windows SANS WSL.
* (car oui, ceux d'entre vous qui auront choisi d'installer WSL au début du cours ont des fonctionnalités supplémentaires)
*
* @param int[] $touches Les touches à détecter
* @return int|null La touche détectée ou null.
*/
function windowsToucheAppuyee(array $touches): ?int
{
static $windows = null;
static $handle = null;
if (null === $windows) {
// Cette définition vient du gist suivant qui détaille beaucoup plus la chose
// https://gist.github.com/Nek-/118cc36d0d075febf614c53a48470490
$windows = \FFI::cdef(<<<C
typedef unsigned short wchar_t;
typedef int BOOL;
typedef unsigned long DWORD;
typedef void *PVOID;
typedef PVOID HANDLE;
typedef DWORD *LPDWORD;
typedef unsigned short WORD;
typedef wchar_t WCHAR;
typedef short SHORT;
typedef unsigned int UINT;
typedef char CHAR;
typedef struct _COORD {
SHORT X;
SHORT Y;
} COORD, *PCOORD;
typedef struct _WINDOW_BUFFER_SIZE_RECORD {
COORD dwSize;
} WINDOW_BUFFER_SIZE_RECORD;
typedef struct _MENU_EVENT_RECORD {
UINT dwCommandId;
} MENU_EVENT_RECORD, *PMENU_EVENT_RECORD;
typedef struct _KEY_EVENT_RECORD {
BOOL bKeyDown;
WORD wRepeatCount;
WORD wVirtualKeyCode;
WORD wVirtualScanCode;
union {
WCHAR UnicodeChar;
CHAR AsciiChar;
} uChar;
DWORD dwControlKeyState;
} KEY_EVENT_RECORD;
typedef struct _MOUSE_EVENT_RECORD {
COORD dwMousePosition;
DWORD dwButtonState;
DWORD dwControlKeyState;
DWORD dwEventFlags;
} MOUSE_EVENT_RECORD;
typedef struct _FOCUS_EVENT_RECORD {
BOOL bSetFocus;
} FOCUS_EVENT_RECORD;
typedef struct _INPUT_RECORD {
WORD EventType;
union {
KEY_EVENT_RECORD KeyEvent;
MOUSE_EVENT_RECORD MouseEvent;
WINDOW_BUFFER_SIZE_RECORD WindowBufferSizeEvent;
MENU_EVENT_RECORD MenuEvent;
FOCUS_EVENT_RECORD FocusEvent;
} Event;
} INPUT_RECORD;
typedef INPUT_RECORD *PINPUT_RECORD;
HANDLE GetStdHandle(DWORD nStdHandle);
BOOL GetConsoleMode(
HANDLE hConsoleHandle,
LPDWORD lpMode
);
BOOL SetConsoleMode(
HANDLE hConsoleHandle,
DWORD dwMode
);
BOOL GetNumberOfConsoleInputEvents(
HANDLE hConsoleInput,
LPDWORD lpcNumberOfEvents
);
BOOL ReadConsoleInputA(
HANDLE hConsoleInput,
PINPUT_RECORD lpBuffer,
DWORD nLength,
LPDWORD lpNumberOfEventsRead
);
BOOL ReadConsoleInputW(
HANDLE hConsoleInput,
PINPUT_RECORD lpBuffer,
DWORD nLength,
LPDWORD lpNumberOfEventsRead
);
BOOL CloseHandle(HANDLE hObject);
C, 'C:\\Windows\\System32\\kernel32.dll');
$handle = $windows->GetStdHandle(STD_INPUT_HANDLE);
$newConsoleMode = ENABLE_WINDOW_INPUT | ENABLE_PROCESSED_INPUT;
if (!$windows->SetConsoleMode($handle, $newConsoleMode)) {
throw new \RuntimeException('Il y a un problème avec la fonction SetConsoleMode: impossible de capturer les entrées...! Si vous avez cette erreur postez un message sur le forum de Zeste de Savoir.');
}
}
$availableCharsInBuffer = $windows->new('DWORD');
$localInputBufferSize = 128;
$localInputBuffer = $windows->new("INPUT_RECORD[$localInputBufferSize]");
$inputInLocalBuffer = $windows->new('DWORD');
$windows->GetNumberOfConsoleInputEvents(
$handle,
\FFI::addr($availableCharsInBuffer)
);
// Le caractère \0 est quasiment toujours disponible mais ne nous intéresse pas !
if ($availableCharsInBuffer->cdata <= 1) {
return null; // Encore la petite technique de retour le plus rapidement possible pour éviter d'avoir un niveau supplémentaire
}
if (! $windows->ReadConsoleInputA($handle, $localInputBuffer, $localInputBufferSize, \FFI::addr($inputInLocalBuffer)) ) {
throw new \RuntimeException('Il y a un problème avec la fonction ReadConsoleInputW: impossible de capturer les entrées...! Si vous avez cette erreur postez un message sur le forum de Zeste de Savoir.');
}
for ($i = $inputInLocalBuffer->cdata - 1; $i >= 0; $i--) {
if ($localInputBuffer[$i]->EventType === KEY_EVENT) {
$keyEvent = $localInputBuffer[$i]->Event->KeyEvent;
//var_dump($keyEvent);
foreach ($touches as $touche => $toucheCode) {
// Pour des raisons évidentes de COMPLEXITE EXTREME DU CODE
// je triche ici est je prends le dernier caractères composant la touche (c'est celui qui diffère) pour faire le test
if ($keyEvent->uChar->AsciiChar === $toucheCode) {
return $touche;
}
}
}
}
return null;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment