Last active
August 8, 2022 15:48
-
-
Save Nek-/119149c2b662ff9580e5d27261f83aef to your computer and use it in GitHub Desktop.
Quelques fonctions pour utiliser le terminal en PHP
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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