Skip to content

Instantly share code, notes, and snippets.

@MircoBabin
Created December 31, 2025 15:24
Show Gist options
  • Select an option

  • Save MircoBabin/05837a3dcbd157035afc56b729b70cf0 to your computer and use it in GitHub Desktop.

Select an option

Save MircoBabin/05837a3dcbd157035afc56b729b70cf0 to your computer and use it in GitHub Desktop.
PHP on Windows - read a key from the keyboard in the CLI.
<?php
/*
PhpWinKeyboard - MIT license - v1.0 - 2025-12-31
------------------------------------------------
Copyright (c) 2025 Mirco Babin
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
*/
/*
EXAMPLE:
--------
require_once(__DIR__.'/PhpWinKeyboard.php');
$keyboard = new \PhpWinKeyboard();
// ------------------------------------------------
// BEGIN clear screen using ANSI escape codes
echo chr(27).'c'; // set terminal to initial state
echo chr(27).'[H'; // goto beginning
echo chr(27).'[J'; // cls
// END clear screen
// ------------------------------------------------
echo 'Dit is één UTF-8 € string.'.PHP_EOL;
echo PHP_EOL;
echo 'Don\'t press a key for 10 seconds to end this example.'.PHP_EOL;
echo PHP_EOL;
while(true)
{
$input = $keyboard->readCharacter(10 * 1000000); // 10 seconds timeout
if ($input === null) {
// no key pressed within 10 seconds --> stop
break;
}
//var_dump($input);
if ($input['keyName'] !== null) {
echo $input['keyName'].PHP_EOL;
} else {
echo 'character: '.$input['character'].PHP_EOL;
}
}
*/
class PhpWinKeyboard
{
private const STD_INPUT_HANDLE = -10;
private const ENABLE_PROCESSED_INPUT = 0x0001;
private const ENABLE_WINDOW_INPUT = 0x0008;
// https://learn.microsoft.com/en-us/windows/console/input-record-str
private const KEY_EVENT = 0x0001;
private const MOUSE_EVENT = 0x0002;
private const WINDOW_BUFFER_SIZE_EVENT = 0x0004;
// MENU_EVENT and FOCUS_EVENT are used internally and should be ignored.
private const CAPSLOCK_ON = 0x0080;
private const ENHANCED_KEY = 0x0100;
private const LEFT_ALT_PRESSED = 0x0002;
private const LEFT_CTRL_PRESSED = 0x0008;
private const NUMLOCK_ON = 0x0020;
private const RIGHT_ALT_PRESSED = 0x0001;
private const RIGHT_CTRL_PRESSED = 0x0004;
private const SCROLLLOCK_ON = 0x0040;
private const SHIFT_PRESSED = 0x0010;
// https://learn.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes
private const VK_BACK = 0x08; // backspace
private const VK_TAB = 0x09; // tab
private const VK_RETURN = 0x0D; // enter
private const VK_ESCAPE = 0x1B; // esc
private const VK_PRIOR = 0x21; // page-up
private const VK_NEXT = 0x22; // page-down
private const VK_END = 0x23; // end
private const VK_HOME = 0x24; // home
private const VK_LEFT = 0x25; // left-arrow
private const VK_UP = 0x26; // up-arrow
private const VK_RIGHT = 0x27; // right-arrow
private const VK_DOWN = 0x28; // down-arrow
private const VK_INSERT = 0x2D; // insert
private const VK_DELETE = 0x2E; // delete
private const VK_F1 = 0x70; // f1
private const VK_F2 = 0x71; // f2
private const VK_F3 = 0x72; // f3
private const VK_F4 = 0x73; // f4
private const VK_F5 = 0x74; // f5
private const VK_F6 = 0x75; // f6
private const VK_F7 = 0x76; // f7
private const VK_F8 = 0x77; // f8
private const VK_F9 = 0x78; // f9
private const VK_F10 = 0x79; // f10
private const VK_F11 = 0x7A; // f11
private const VK_F12 = 0x7B; // f12
private static $virtualKeyCode_To_keyNameMapping = [
self::VK_BACK => 'backspace',
self::VK_TAB => 'tab',
self::VK_RETURN => 'enter',
self::VK_ESCAPE => 'esc',
self::VK_PRIOR => 'page-up',
self::VK_NEXT => 'page-down',
self::VK_END => 'end',
self::VK_HOME => 'home',
self::VK_LEFT => 'left-arrow',
self::VK_UP => 'up-arrow',
self::VK_RIGHT => 'right-arrow',
self::VK_DOWN => 'down-arrow',
self::VK_INSERT => 'insert',
self::VK_DELETE => 'delete',
self::VK_F1 => 'f1',
self::VK_F2 => 'f2',
self::VK_F3 => 'f3',
self::VK_F4 => 'f4',
self::VK_F5 => 'f5',
self::VK_F6 => 'f6',
self::VK_F7 => 'f7',
self::VK_F8 => 'f8',
self::VK_F9 => 'f9',
self::VK_F10 => 'f10',
self::VK_F11 => 'f11',
self::VK_F12 => 'f12',
];
private \FFI $ffi;
private $ffi_stdin_handle;
private $ffi_inputBuffer;
private $ffi_unreadInputRecords;
private $ffi_readInputRecords;
public function __construct()
{
if (DIRECTORY_SEPARATOR !== '\\') {
throw new \Exception('PhpWinKeyboard - this class is only usable on a Windows OS.');
}
if (php_sapi_name() !== 'cli') {
throw new \Exception('PhpWinKeyboard - this class is only usable in the CLI. Start php.exe using the command prompt.');
}
if (!class_exists('\FFI')) {
throw new \Exception('PhpWinKeyboard - FFI extension is not loaded. Adjust php.ini.');
}
if (!function_exists('mb_convert_encoding')) {
throw new \Exception('PhpWinKeyboard - MBSTRING extension is not loaded. Adjust php.ini.');
}
$cdef = <<<'EOCDEF'
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 _KEY_EVENT_RECORD {
BOOL bKeyDown;
WORD wRepeatCount;
WORD wVirtualKeyCode;
WORD wVirtualScanCode;
union {
WCHAR UnicodeChar;
CHAR AsciiChar;
} uChar;
DWORD dwControlKeyState;
} KEY_EVENT_RECORD;
typedef struct _COORD {
SHORT X;
SHORT Y;
} COORD;
typedef struct _MOUSE_EVENT_RECORD {
COORD dwMousePosition;
DWORD dwButtonState;
DWORD dwControlKeyState;
DWORD dwEventFlags;
} MOUSE_EVENT_RECORD;
typedef struct _WINDOW_BUFFER_SIZE_RECORD {
COORD dwSize;
} WINDOW_BUFFER_SIZE_RECORD;
typedef struct _MENU_EVENT_RECORD {
UINT dwCommandId;
} MENU_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(
/* _In_ */ HANDLE hConsoleHandle,
/* _Out_ */ LPDWORD lpMode
);
BOOL SetConsoleMode(
/* _In_ */ HANDLE hConsoleHandle,
/* _In_ */ DWORD dwMode
);
BOOL GetNumberOfConsoleInputEvents(
/* _In_ */ HANDLE hConsoleInput,
/* _Out_ */ LPDWORD lpcNumberOfEvents
);
BOOL ReadConsoleInputW(
/* _In_ */ HANDLE hConsoleInput,
/* _Out_ */ PINPUT_RECORD lpBuffer,
/* _In_ */ DWORD nLength,
/* _Out_ */ LPDWORD lpNumberOfEventsRead
);
EOCDEF;
$cdefDLL = 'kernel32.dll';
$ffi = \FFI::cdef($cdef, $cdefDLL);
if ($ffi === null) {
throw new \Exception('PhpWinKeyboard - FFI initialization failed.');
}
$this->ffi = $ffi;
$this->ffi_stdin_handle = $this->ffi->GetStdHandle(self::STD_INPUT_HANDLE);
// TODO: check if $this->ffi_stdin_handle === INVALID_HANDLE_VALUE then throw new \Exception('PhpWinKeyboard - Error getting STD_INPUT_HANDLE.')
$this->ffi_inputBuffer = $this->ffi->new('INPUT_RECORD[1]');
$this->ffi_unreadInputRecords = $this->ffi->new('DWORD');
$this->ffi_readInputRecords = $this->ffi->new('DWORD');
}
public function readCharacter(?int $microSecondsTimeout = null)
{
$oldConsoleMode = $this->ffi->new('DWORD');
if (!$this->ffi->GetConsoleMode($this->ffi_stdin_handle, \FFI::addr($oldConsoleMode))) {
throw new \Exception('PhpWinKeyboard - Error getting current console mode.');
}
$oldConsoleMode = $oldConsoleMode->cdata;
$newConsoleMode = self::ENABLE_WINDOW_INPUT | self::ENABLE_PROCESSED_INPUT;
if (!$this->ffi->SetConsoleMode($this->ffi_stdin_handle, $newConsoleMode)) {
throw new \Exception('PhpWinKeyboard - Error setting console mode to ENABLE_WINDOW_INPUT | ENABLE_PROCESSED_INPUT.');
}
try {
$start = hrtime(true);
while (true) {
$this->ffi_unreadInputRecords->cdata = 0;
if (!$this->ffi->GetNumberOfConsoleInputEvents($this->ffi_stdin_handle, \FFI::addr($this->ffi_unreadInputRecords))) {
throw new \Exception('PhpWinKeyboard - Error getting number of console input events.');
}
if ($this->ffi_unreadInputRecords->cdata > 0) {
$this->ffi_readInputRecords->cdata = 0;
if (!$this->ffi->ReadConsoleInputW($this->ffi_stdin_handle, $this->ffi_inputBuffer, 1, \FFI::addr($this->ffi_readInputRecords))) {
throw new \Exception('PhpWinKeyboard - Error reading console input.');
}
if ($this->ffi_readInputRecords->cdata !== 1) {
throw new \Exception('PhpWinKeyboard - Error reading console input, '.$this->ffi_readInputRecords->cdata.' events were read.');
}
$event = $this->ffi_inputBuffer[0];
switch($event->EventType) {
case self::KEY_EVENT:
$keyEvent = $event->Event->KeyEvent;
$result = $this->handle_KEY_EVENT($keyEvent);
if ($result !== null) {
return $result;
}
break;
}
}
if ($microSecondsTimeout !== null) {
// 1 microsecond is one millionth of a second.
// 1 / 1_000_000 of a second.
// 1 nanosecond is one thousand-millionth of a second.
// 1 / 1_000_000_000 of a second.
$elapsed = hrtime(true) - $start; // in nanoseconds
if ($elapsed / 1000 >= $microSecondsTimeout) {
return null;
}
}
}
} finally {
if (!$this->ffi->SetConsoleMode($this->ffi_stdin_handle, $oldConsoleMode)) {
throw new \Exception('PhpWinKeyboard - Error restoring console mode.');
}
}
}
private function handle_KEY_EVENT($keyEvent)
{
$keyDown = ($keyEvent->bKeyDown ? true : false);
if (!$keyDown) {
return null;
}
$virtualKeyCode = $keyEvent->wVirtualKeyCode;
$result = [
'character' => '',
'virtualKeyCode' => $virtualKeyCode,
'keyName' => null,
'controlKeyState' => [
'altPressed' => (($keyEvent->dwControlKeyState & (self::LEFT_ALT_PRESSED | self::RIGHT_ALT_PRESSED)) > 0),
'ctrlPressed' => (($keyEvent->dwControlKeyState & (self::LEFT_CTRL_PRESSED | self::RIGHT_CTRL_PRESSED)) > 0),
'shiftPressed' => (($keyEvent->dwControlKeyState & self::SHIFT_PRESSED) > 0),
'capsLockOn' => (($keyEvent->dwControlKeyState & self::CAPSLOCK_ON) > 0),
'numLockOn' => (($keyEvent->dwControlKeyState & self::NUMLOCK_ON) > 0),
'scrollLockOn' => (($keyEvent->dwControlKeyState & self::SCROLLLOCK_ON) > 0),
'enhancedKey' => (($keyEvent->dwControlKeyState & self::ENHANCED_KEY) > 0),
'leftAltPressed' => (($keyEvent->dwControlKeyState & self::LEFT_ALT_PRESSED) > 0),
'leftCtrlPressed' => (($keyEvent->dwControlKeyState & self::LEFT_CTRL_PRESSED) > 0),
'rightAltPressed' => (($keyEvent->dwControlKeyState & self::RIGHT_ALT_PRESSED) > 0),
'rightCtrlPressed' => (($keyEvent->dwControlKeyState & self::RIGHT_CTRL_PRESSED) > 0),
],
];
if (isset(self::$virtualKeyCode_To_keyNameMapping[$virtualKeyCode])) {
$result['keyName'] = self::$virtualKeyCode_To_keyNameMapping[$virtualKeyCode];
}
$character = $keyEvent->uChar->UnicodeChar;
if ($character === 0) {
if ($result['keyName'] !== null) {
return $result;
}
return null;
}
$utf16le = pack('v', $character);
if ($character >= 0xD800 && $character <= 0xDBFF) {
// UTF16-LE high surrogate character, read the next event containing the low surrogate character.
// Together they form the surrogate pair.
$this->ffi_readInputRecords->cdata = 0;
if (!$this->ffi->ReadConsoleInputW($this->ffi_stdin_handle, $this->ffi_inputBuffer, 1, \FFI::addr($this->ffi_readInputRecords))) {
throw new \Exception('PhpWinKeyboard - Error reading console input (surrogate pair).');
}
if ($this->ffi_readInputRecords->cdata !== 1) {
throw new \Exception('PhpWinKeyboard - Error reading console input (surrogate pair), '.$this->ffi_readInputRecords->cdata.' events were read.');
}
$event = $this->ffi_inputBuffer[0];
if ($event->EventType !== self::KEY_EVENT) {
throw new \Exception('PhpWinKeyboard - Error reading console input (surrogate pair), did not receive a KEY_EVENT.');
}
$keyEvent = $event->Event->KeyEvent;
$character = $keyEvent->uChar->UnicodeChar;
$utf16le .= pack('v', $character);
}
$result['character'] = mb_convert_encoding($utf16le, 'UTF-8', 'UTF-16LE');
return $result;
}
}
@MircoBabin
Copy link
Author

MircoBabin commented Dec 31, 2025

This class was inspired by https://github.com/nahkampf/php-keypress-windows. I wanted a standalone, one file class. I have tested it on Windows/11 with PHP 8.2 and it seems to work.

Unfortunately PHP on Windows lacks a mechanism in the CLI to read exactly one character from the keyboard. The readline for Windows is missing readline_callback_handler_install() and readline_callback_read_char() functions. Making it impossible on Windows to read exactly one keypress from the keyboard, without having to wait for the ENTER key.

This class provides via FFI this keypress functionality. The purpose of this class is education. This class is used for educating PHP as the first programming language using only the CLI. When educating some basic text input/output is needed. So students can play around with input (keypress) and do something. Making a simple text game using arrow-key movement is now possible.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment