<?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\Exception; class Windows extends Virtual { /** * Whether or not mbstring is enabled * * @var null|bool */ protected static $hasMBString; /** * Results of probing system capabilities * * @var mixed */ protected $probeResult; /** * 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 "mode" command if ($this->probeResult === null) { $this->runProbeCommand(); } if (count($this->probeResult) && (int) $this->probeResult[0]) { $width = (int) $this->probeResult[0]; } else { $width = parent::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 "mode" command if ($this->probeResult === null) { $this->runProbeCommand(); } if (count($this->probeResult) && (int) $this->probeResult[1]) { $height = (int) $this->probeResult[1]; } else { $height = parent::getheight(); } return $height; } /** * Probe for system capabilities and cache results * * Run a Windows Powershell command that determines parameters of console window. The command is fed through * standard input (with echo) to prevent Powershell from creating a sub-thread and hanging PHP when run through * a debugger/IDE. * * @return void */ protected function runProbeCommand() { exec( 'echo $size = $Host.ui.rawui.windowsize; write $($size.width) $($size.height) | powershell -NonInteractive -NoProfile -NoLogo -OutputFormat Text -Command -', $output, $return ); if ($return || empty($output)) { $this->probeResult = ''; } else { $this->probeResult = $output; } } /** * 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"); } /** * Set Console charset to use. * * @param Charset\CharsetInterface $charset */ public function setCharset(Charset\CharsetInterface $charset) { $this->charset = $charset; } /** * Get charset currently in use by this adapter. * * @return Charset\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; } /** * Switch to utf-8 encoding * * @return void */ protected function switchToUtf8() { shell_exec('mode con cp select=65001'); } /** * Clear console screen */ public function clear() { // Attempt to clear the screen using PowerShell command exec("powershell -NonInteractive -NoProfile -NoLogo -OutputFormat Text -Command Clear-Host", $output, $return); if ($return) { // Could not run powershell... fall back to filling the buffer with newlines echo str_repeat("\r\n", $this->getHeight()); } } /** * Clear line at cursor position */ public function clearLine() { echo "\r" . str_repeat(' ', $this->getWidth()) . "\r"; } /** * Read a single character from the console input * * @param string|null $mask A list of allowed chars * @throws Exception\RuntimeException * @return string */ 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 95+ "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))) { // Normalize CR to LF if ($char == 13) { $char = 10; } // Convert to a char $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; } /** * Read a single line from the console input. * * @param int $maxLength Maximum response length * @return string */ public function readLine($maxLength = 2048) { $f = fopen('php://stdin','r'); $line = rtrim(fread($f, $maxLength),"\r\n"); fclose($f); return $line; } }