<?php /** * Zend Framework (http://framework.zend.com/) * * @link http://github.com/zendframework/zf2 for the canonical source repository * @copyright Copyright (c) 2005-2014 Zend Technologies USA Inc. (http://www.zend.com) * @license http://framework.zend.com/license/new-bsd New BSD License */ namespace Zend\Console\Adapter; use Zend\Console\Charset; use Zend\Console\Charset\CharsetInterface; use Zend\Console\Exception; /** * MS Windows with ANSICON console adapter * * This adapter requires ANSICON extension to be installed. It can be obtained from: * https://github.com/adoxa/ansicon * * ANSICON has to be loaded and enabled before using this adapter. It's best to install * it using following command: * ansicon -I * * Console should not run in UTF8 code page (65001), because ANSICON does not behave well with it. * It's best to use non-unicode code page 437, 850, 851, 852 or similar. Run "help mode" for more * information on how to change Windows console code page. */ class WindowsAnsicon extends Posix { /** * Whether or not mbstring is enabled * * @var null|bool */ protected static $hasMBString; /** * Results of mode command * * @var mixed */ protected $modeResult; /** * Determine and return current console width. * * @return int */ public function getWidth() { static $width; if ($width > 0) { return $width; } // Try to read console size from ANSICON env var if (preg_match('/\((\d+)x/', getenv('ANSICON'), $matches)) { $width = $matches[1]; } else { $width = AbstractAdapter::getWidth(); } return $width; } /** * Determine and return current console height. * * @return false|int */ public function getHeight() { static $height; if ($height > 0) { return $height; } // Try to read console size from ANSICON env var if (preg_match('/\(\d+x(\d+)/', getenv('ANSICON'), $matches)) { $height = $matches[1]; } else { $height = AbstractAdapter::getHeight(); } return $height; } /** * Run and cache results of mode command * * @return void */ protected function runModeCommand() { exec('mode', $output, $return); if ($return || !count($output)) { $this->modeResult = ''; } else { $this->modeResult = trim(implode('', $output)); } } /** * Check if console is UTF-8 compatible * * @return bool */ public function isUtf8() { // Try to read code page info from "mode" command if ($this->modeResult === null) { $this->runModeCommand(); } if (preg_match('/Code page\:\s+(\d+)/', $this->modeResult, $matches)) { return (int) $matches[1] == 65001; } return false; } /** * Return current console window title. * * @return string */ public function getTitle() { // Try to use powershell to retrieve console window title exec('powershell -command "write $Host.UI.RawUI.WindowTitle"', $output, $result); if ($result || !$output) { return ''; } return trim($output, "\r\n"); } /** * Clear console screen */ public function clear() { echo chr(27) . '[1J' . chr(27) . '[u'; } /** * Clear line at cursor position */ public function clearLine() { echo chr(27) . '[1K'; } /** * Set Console charset to use. * * @param CharsetInterface $charset */ public function setCharset(CharsetInterface $charset) { $this->charset = $charset; } /** * Get charset currently in use by this adapter. * * @return CharsetInterface $charset */ public function getCharset() { if ($this->charset === null) { $this->charset = $this->getDefaultCharset(); } return $this->charset; } /** * @return Charset\AsciiExtended */ public function getDefaultCharset() { return new Charset\AsciiExtended(); } /** * Read a single character from the console input * * @param string|null $mask A list of allowed chars * @return string * @throws Exception\RuntimeException */ public function readChar($mask = null) { // Decide if we can use `choice` tool $useChoice = $mask !== null && preg_match('/^[a-zA-Z0-9]+$/D', $mask); if ($useChoice) { // Use Windows 98+ "choice" command, which allows for reading a // single character matching a mask, but is limited to lower ASCII // range. do { exec('choice /n /cs /c:' . $mask, $output, $return); if ($return == 255 || $return < 1 || $return > strlen($mask)) { throw new Exception\RuntimeException('"choice" command failed to run. Are you using Windows XP or newer?'); } // Fetch the char from mask $char = substr($mask, $return - 1, 1); } while ("" === $char || ($mask !== null && false === strstr($mask, $char))); return $char; } // Try to use PowerShell, giving it console access. Because PowersShell // interpreter can take a short while to load, we are emptying the // whole keyboard buffer and picking the last key that has been pressed // before or after PowerShell command has started. The ASCII code for // that key is then converted to a character. if ($mask === null) { exec( 'powershell -NonInteractive -NoProfile -NoLogo -OutputFormat Text -Command "' . 'while ($Host.UI.RawUI.KeyAvailable) {$key = $Host.UI.RawUI.ReadKey(\'NoEcho,IncludeKeyDown\');}' . 'write $key.VirtualKeyCode;' . '"', $result, $return ); // Retrieve char from the result. $char = !empty($result) ? implode('', $result) : null; if (!empty($char) && !$return) { // We have obtained an ASCII code, convert back to a char ... $char = chr($char); // ... and return it... return $char; } } else { // Windows and DOS will return carriage-return char (ASCII 13) when // the user presses [ENTER] key, but Console Adapter user might // have provided a \n Newline (ASCII 10) in the mask, to allow // [ENTER]. We are going to replace all CR with NL to conform. $mask = strtr($mask, "\n", "\r"); // Prepare a list of ASCII codes from mask chars $asciiMask = array_map(function ($char) { return ord($char); }, str_split($mask)); $asciiMask = array_unique($asciiMask); // Char mask filtering is now handled by the PowerShell itself, // because it's a much faster method than invoking PS interpreter // after each mismatch. The command should return ASCII code of a // matching key. $result = $return = null; exec( 'powershell -NonInteractive -NoProfile -NoLogo -OutputFormat Text -Command "' . '[int[]] $mask = '.join(',', $asciiMask).';' . 'do {' . '$key = $Host.UI.RawUI.ReadKey(\'NoEcho,IncludeKeyDown\').VirtualKeyCode;' . '} while( !($mask -contains $key) );' . 'write $key;' . '"', $result, $return ); $char = !empty($result) ? trim(implode('', $result)) : null; if (!$return && $char && ($mask === null || in_array($char, $asciiMask))) { // We have obtained an ASCII code, check if it is a carriage // return and normalize it as needed if ($char == 13) { $char = 10; } // Convert to a character $char = chr($char); // ... and return it... return $char; } } // Fall back to standard input, which on Windows does not allow reading // a single character. This is a limitation of Windows streams // implementation (not PHP) and this behavior cannot be changed with a // command like "stty", known to POSIX systems. $stream = fopen('php://stdin', 'rb'); do { $char = fgetc($stream); $char = substr(trim($char), 0, 1); } while (!$char || ($mask !== null && !stristr($mask, $char))); fclose($stream); return $char; } }