1<?php
2/**
3 * Zend Framework (http://framework.zend.com/)
4 *
5 * @link      http://github.com/zendframework/zf2 for the canonical source repository
6 * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
7 * @license   http://framework.zend.com/license/new-bsd New BSD License
8 */
9
10namespace Zend\Console\Adapter;
11
12use Zend\Console\Charset;
13use Zend\Console\Exception;
14use Zend\Stdlib\StringUtils;
15
16/**
17 * Common console adapter codebase
18 */
19abstract class AbstractAdapter implements AdapterInterface
20{
21    /**
22     * Whether or not mbstring is enabled
23     *
24     * @var null|bool
25     */
26    protected static $hasMBString;
27
28    /**
29     * @var Charset\CharsetInterface
30     */
31    protected $charset;
32
33    /**
34     * Current cursor X position
35     *
36     * @var int
37     */
38    protected $posX;
39
40    /**
41     * Current cursor Y position
42     *
43     * @var int
44     */
45    protected $posY;
46
47    /**
48     * Write a chunk of text to console.
49     *
50     * @param string   $text
51     * @param null|int $color
52     * @param null|int $bgColor
53     */
54    public function write($text, $color = null, $bgColor = null)
55    {
56        //Encode text to match console encoding
57        $text = $this->encodeText($text);
58
59        if ($color !== null || $bgColor !== null) {
60            echo $this->colorize($text, $color, $bgColor);
61        } else {
62            echo $text;
63        }
64    }
65
66    /**
67     * Alias for write()
68     *
69     * @param string   $text
70     * @param null|int $color
71     * @param null|int $bgColor
72     */
73    public function writeText($text, $color = null, $bgColor = null)
74    {
75        return $this->write($text, $color, $bgColor);
76    }
77
78    /**
79     * Write a single line of text to console and advance cursor to the next line.
80     *
81     * @param string   $text
82     * @param null|int $color
83     * @param null|int $bgColor
84     */
85    public function writeLine($text = "", $color = null, $bgColor = null)
86    {
87        $this->write($text . PHP_EOL, $color, $bgColor);
88    }
89
90    /**
91     * Write a piece of text at the coordinates of $x and $y
92     *
93     *
94     * @param string   $text    Text to write
95     * @param int      $x       Console X coordinate (column)
96     * @param int      $y       Console Y coordinate (row)
97     * @param null|int $color
98     * @param null|int $bgColor
99     */
100    public function writeAt($text, $x, $y, $color = null, $bgColor = null)
101    {
102        $this->setPos($x, $y);
103        $this->write($text, $color, $bgColor);
104    }
105
106    /**
107     * Write a box at the specified coordinates.
108     * If X or Y coordinate value is negative, it will be calculated as the distance from far right or bottom edge
109     * of the console (respectively).
110     *
111     * @param int      $x1           Top-left corner X coordinate (column)
112     * @param int      $y1           Top-left corner Y coordinate (row)
113     * @param int      $x2           Bottom-right corner X coordinate (column)
114     * @param int      $y2           Bottom-right corner Y coordinate (row)
115     * @param int      $lineStyle    (optional) Box border style.
116     * @param int      $fillStyle    (optional) Box fill style or a single character to fill it with.
117     * @param int      $color        (optional) Foreground color
118     * @param int      $bgColor      (optional) Background color
119     * @param null|int $fillColor    (optional) Foreground color of box fill
120     * @param null|int $fillBgColor  (optional) Background color of box fill
121     * @throws Exception\BadMethodCallException if coordinates are invalid
122     */
123    public function writeBox(
124        $x1,
125        $y1,
126        $x2,
127        $y2,
128        $lineStyle = self::LINE_SINGLE,
129        $fillStyle = self::FILL_NONE,
130        $color = null,
131        $bgColor = null,
132        $fillColor = null,
133        $fillBgColor = null
134    ) {
135        // Sanitize coordinates
136        $x1 = (int) $x1;
137        $y1 = (int) $y1;
138        $x2 = (int) $x2;
139        $y2 = (int) $y2;
140
141        // Translate negative coordinates
142        if ($x2 < 0) {
143            $x2 = $this->getWidth() - $x2;
144        }
145
146        if ($y2 < 0) {
147            $y2 = $this->getHeight() - $y2;
148        }
149
150        // Validate coordinates
151        if ($x1 < 0
152            || $y1 < 0
153            || $x2 < $x1
154            || $y2 < $y1
155       ) {
156            throw new Exception\BadMethodCallException('Supplied X,Y coordinates are invalid.');
157        }
158
159        // Determine charset and dimensions
160        $charset = $this->getCharset();
161        $width   = $x2 - $x1 + 1;
162
163        if ($width <= 2) {
164            $lineStyle = static::LINE_NONE;
165        }
166
167        // Activate line drawing
168        $this->write($charset::ACTIVATE);
169
170        // Draw horizontal lines
171        if ($lineStyle !== static::LINE_NONE) {
172            switch ($lineStyle) {
173                case static::LINE_SINGLE:
174                    $lineChar = $charset::LINE_SINGLE_EW;
175                    break;
176
177                case static::LINE_DOUBLE:
178                    $lineChar = $charset::LINE_DOUBLE_EW;
179                    break;
180
181                case static::LINE_BLOCK:
182                default:
183                    $lineChar = $charset::LINE_BLOCK_EW;
184                    break;
185            }
186
187            $this->setPos($x1 + 1, $y1);
188            $this->write(str_repeat($lineChar, $width - 2), $color, $bgColor);
189            $this->setPos($x1 + 1, $y2);
190            $this->write(str_repeat($lineChar, $width - 2), $color, $bgColor);
191        }
192
193        // Draw vertical lines and fill
194        if (is_numeric($fillStyle)
195            && $fillStyle !== static::FILL_NONE) {
196            switch ($fillStyle) {
197                case static::FILL_SHADE_LIGHT:
198                    $fillChar = $charset::SHADE_LIGHT;
199                    break;
200                case static::FILL_SHADE_MEDIUM:
201                    $fillChar = $charset::SHADE_MEDIUM;
202                    break;
203                case static::FILL_SHADE_DARK:
204                    $fillChar = $charset::SHADE_DARK;
205                    break;
206                case static::FILL_BLOCK:
207                default:
208                    $fillChar = $charset::BLOCK;
209                    break;
210            }
211        } elseif ($fillStyle) {
212            $fillChar = StringUtils::getWrapper()->substr($fillStyle, 0, 1);
213        } else {
214            $fillChar = ' ';
215        }
216
217        if ($lineStyle === static::LINE_NONE) {
218            for ($y = $y1; $y <= $y2; $y++) {
219                $this->setPos($x1, $y);
220                $this->write(str_repeat($fillChar, $width), $fillColor, $fillBgColor);
221            }
222        } else {
223            switch ($lineStyle) {
224                case static::LINE_DOUBLE:
225                    $lineChar = $charset::LINE_DOUBLE_NS;
226                    break;
227                case static::LINE_BLOCK:
228                    $lineChar = $charset::LINE_BLOCK_NS;
229                    break;
230                case static::LINE_SINGLE:
231                default:
232                    $lineChar = $charset::LINE_SINGLE_NS;
233                    break;
234            }
235
236            for ($y = $y1 + 1; $y < $y2; $y++) {
237                $this->setPos($x1, $y);
238                $this->write($lineChar, $color, $bgColor);
239                $this->write(str_repeat($fillChar, $width - 2), $fillColor, $fillBgColor);
240                $this->write($lineChar, $color, $bgColor);
241            }
242        }
243
244        // Draw corners
245        if ($lineStyle !== static::LINE_NONE) {
246            if ($color !== null) {
247                $this->setColor($color);
248            }
249            if ($bgColor !== null) {
250                $this->setBgColor($bgColor);
251            }
252            if ($lineStyle === static::LINE_SINGLE) {
253                $this->writeAt($charset::LINE_SINGLE_NW, $x1, $y1);
254                $this->writeAt($charset::LINE_SINGLE_NE, $x2, $y1);
255                $this->writeAt($charset::LINE_SINGLE_SE, $x2, $y2);
256                $this->writeAt($charset::LINE_SINGLE_SW, $x1, $y2);
257            } elseif ($lineStyle === static::LINE_DOUBLE) {
258                $this->writeAt($charset::LINE_DOUBLE_NW, $x1, $y1);
259                $this->writeAt($charset::LINE_DOUBLE_NE, $x2, $y1);
260                $this->writeAt($charset::LINE_DOUBLE_SE, $x2, $y2);
261                $this->writeAt($charset::LINE_DOUBLE_SW, $x1, $y2);
262            } elseif ($lineStyle === static::LINE_BLOCK) {
263                $this->writeAt($charset::LINE_BLOCK_NW, $x1, $y1);
264                $this->writeAt($charset::LINE_BLOCK_NE, $x2, $y1);
265                $this->writeAt($charset::LINE_BLOCK_SE, $x2, $y2);
266                $this->writeAt($charset::LINE_BLOCK_SW, $x1, $y2);
267            }
268        }
269
270        // Deactivate line drawing and reset colors
271        $this->write($charset::DEACTIVATE);
272        $this->resetColor();
273    }
274
275    /**
276     * Write a block of text at the given coordinates, matching the supplied width and height.
277     * In case a line of text does not fit desired width, it will be wrapped to the next line.
278     * In case the whole text does not fit in desired height, it will be truncated.
279     *
280     * @param string   $text    Text to write
281     * @param int      $width   Maximum block width. Negative value means distance from right edge.
282     * @param int|null $height  Maximum block height. Negative value means distance from bottom edge.
283     * @param int      $x       Block X coordinate (column)
284     * @param int      $y       Block Y coordinate (row)
285     * @param null|int $color   (optional) Text color
286     * @param null|int $bgColor (optional) Text background color
287     * @throws Exception\InvalidArgumentException
288     */
289    public function writeTextBlock(
290        $text,
291        $width,
292        $height = null,
293        $x = 0,
294        $y = 0,
295        $color = null,
296        $bgColor = null
297    ) {
298        if ($x < 0 || $y < 0) {
299            throw new Exception\InvalidArgumentException('Supplied X,Y coordinates are invalid.');
300        }
301
302        if ($width < 1) {
303            throw new Exception\InvalidArgumentException('Invalid width supplied.');
304        }
305
306        if (null !== $height && $height < 1) {
307            throw new Exception\InvalidArgumentException('Invalid height supplied.');
308        }
309
310        // ensure the text is not wider than the width
311        if (strlen($text) <= $width) {
312            // just write the line at the spec'd position
313            $this->setPos($x, $y);
314            $this->write($text, $color, $bgColor);
315            return;
316        }
317
318        $text = wordwrap($text, $width, PHP_EOL, true);
319
320        // convert to array of lines
321        $lines = explode(PHP_EOL, $text);
322
323        // truncate if height was specified
324        if (null !== $height && count($lines) > $height) {
325            $lines = array_slice($lines, 0, $height);
326        }
327
328        // write each line
329        $curY = $y;
330        foreach ($lines as $line) {
331            $this->setPos($x, $curY);
332            $this->write($line, $color, $bgColor);
333            $curY++;//next line
334        }
335    }
336
337    /**
338     * Determine and return current console width.
339     *
340     * @return int
341     */
342    public function getWidth()
343    {
344        return 80;
345    }
346
347    /**
348     * Determine and return current console height.
349     *
350     * @return int
351     */
352    public function getHeight()
353    {
354        return 25;
355    }
356
357    /**
358     * Determine and return current console width and height.
359     *
360     * @return int[] array($width, $height)
361     */
362    public function getSize()
363    {
364        return array(
365            $this->getWidth(),
366            $this->getHeight(),
367        );
368    }
369
370    /**
371     * Check if console is UTF-8 compatible
372     *
373     * @return bool
374     */
375    public function isUtf8()
376    {
377        return true;
378    }
379
380    /**
381     * Set cursor position
382     *
383     * @param int $x
384     * @param int $y
385     */
386    public function setPos($x, $y)
387    {
388    }
389
390    /**
391     * Show console cursor
392     */
393    public function showCursor()
394    {
395    }
396
397    /**
398     * Hide console cursor
399     */
400    public function hideCursor()
401    {
402    }
403
404    /**
405     * Return current console window title.
406     *
407     * @return string
408     */
409    public function getTitle()
410    {
411        return '';
412    }
413
414    /**
415     * Prepare a string that will be rendered in color.
416     *
417     * @param  string   $string
418     * @param  int      $color
419     * @param  null|int $bgColor
420     * @return string
421     */
422    public function colorize($string, $color = null, $bgColor = null)
423    {
424        return $string;
425    }
426
427    /**
428     * Change current drawing color.
429     *
430     * @param int $color
431     */
432    public function setColor($color)
433    {
434    }
435
436    /**
437     * Change current drawing background color
438     *
439     * @param int $color
440     */
441    public function setBgColor($color)
442    {
443    }
444
445    /**
446     * Reset color to console default.
447     */
448    public function resetColor()
449    {
450    }
451
452    /**
453     * Set Console charset to use.
454     *
455     * @param Charset\CharsetInterface $charset
456     */
457    public function setCharset(Charset\CharsetInterface $charset)
458    {
459        $this->charset = $charset;
460    }
461
462    /**
463     * Get charset currently in use by this adapter.
464     *
465     * @return Charset\CharsetInterface $charset
466     */
467    public function getCharset()
468    {
469        if ($this->charset === null) {
470            $this->charset = $this->getDefaultCharset();
471        }
472
473        return $this->charset;
474    }
475
476    /**
477     * @return Charset\Utf8
478     */
479    public function getDefaultCharset()
480    {
481        return new Charset\Utf8;
482    }
483
484    /**
485     * Clear console screen
486     */
487    public function clear()
488    {
489        echo "\f";
490    }
491
492    /**
493     * Clear line at cursor position
494     */
495    public function clearLine()
496    {
497        echo "\r" . str_repeat(" ", $this->getWidth()) . "\r";
498    }
499
500    /**
501     * Clear console screen
502     */
503    public function clearScreen()
504    {
505        return $this->clear();
506    }
507
508    /**
509     * Read a single line from the console input
510     *
511     * @param int $maxLength        Maximum response length
512     * @return string
513     */
514    public function readLine($maxLength = 2048)
515    {
516        $f    = fopen('php://stdin', 'r');
517        $line = stream_get_line($f, $maxLength, PHP_EOL);
518        fclose($f);
519        return rtrim($line, "\n\r");
520    }
521
522    /**
523     * Read a single character from the console input
524     *
525     * @param string|null   $mask   A list of allowed chars
526     * @return string
527     */
528    public function readChar($mask = null)
529    {
530        $f = fopen('php://stdin', 'r');
531        do {
532            $char = fread($f, 1);
533        } while ("" === $char || ($mask !== null && false === strstr($mask, $char)));
534        fclose($f);
535        return $char;
536    }
537
538    /**
539     * Encode a text to match console encoding
540     *
541     * @param  string $text
542     * @return string the encoding text
543     */
544    public function encodeText($text)
545    {
546        if ($this->isUtf8()) {
547            if (StringUtils::isValidUtf8($text)) {
548                return $text;
549            }
550
551            return utf8_encode($text);
552        }
553
554        if (StringUtils::isValidUtf8($text)) {
555            return utf8_decode($text);
556        }
557
558        return $text;
559    }
560}
561