Completed
Push — master ( df8184...e3b994 )
by Aydin
02:31
created

src/MenuStyle.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
namespace PhpSchool\CliMenu;
4
5
use PhpSchool\CliMenu\Exception\InvalidInstantiationException;
6
use PhpSchool\CliMenu\Terminal\TerminalFactory;
7
use PhpSchool\CliMenu\Util\ColourUtil;
8
use PhpSchool\Terminal\Terminal;
9
use Assert\Assertion;
10
11
//TODO: B/W fallback
12
13
/**
14
 * @author Michael Woodward <[email protected]>
15
 */
16
class MenuStyle
17
{
18
    /**
19
     * @var Terminal
20
     */
21
    protected $terminal;
22
23
    /**
24
     * @var string
25
     */
26
    protected $fg;
27
28
    /**
29
     * @var string
30
     */
31
    protected $bg;
32
33
    /**
34
     * @var int
35
     */
36
    protected $width;
37
38
    /**
39
     * @var int
40
     */
41
    protected $margin;
42
43
    /**
44
     * @var int
45
     */
46
    protected $paddingTopBottom;
47
48
    /**
49
     * @var int
50
     */
51
    protected $paddingLeftRight;
52
53
    /**
54
     * @var array
55
     */
56
    private $paddingTopBottomRows = [];
57
58
    /**
59
     * @var int
60
     */
61
    protected $contentWidth;
62
63
    /**
64
     * @var string
65
     */
66
    private $selectedMarker;
67
68
    /**
69
     * @var string
70
     */
71
    private $unselectedMarker;
72
73
    /**
74
     * @var string
75
     */
76
    private $itemExtra;
77
78
    /**
79
     * @var bool
80
     */
81
    private $displaysExtra;
82
83
    /**
84
     * @var string
85
     */
86
    private $titleSeparator;
87
88
    /**
89
     * @var string
90
     */
91
    private $coloursSetCode;
92
93
    /**
94
     * @var string
95
     */
96
    private $invertedColoursSetCode = "\033[7m";
97
98
    /**
99
     * @var string
100
     */
101
    private $invertedColoursUnsetCode = "\033[27m";
102
103
    /**
104
     * @var string
105
     */
106
    private $coloursResetCode = "\033[0m";
107
108
    /**
109
     * @var int
110
     */
111
    private $borderTopWidth;
112
113
    /**
114
     * @var int
115
     */
116
    private $borderRightWidth;
117
118
    /**
119
     * @var int
120
     */
121
    private $borderBottomWidth;
122
123
    /**
124
     * @var int
125
     */
126
    private $borderLeftWidth;
127
128
    /**
129
     * @var string
130
     */
131
    private $borderColour = 'white';
132
133
    /**
134
     * @var array
135
     */
136
    private $borderTopRows = [];
137
138
    /**
139
     * @var array
140
     */
141
    private $borderBottomRows = [];
142
143
    /**
144
     * @var bool
145
     */
146
    private $marginAuto = false;
147
148
    /**
149
     * Default Values
150
     *
151
     * @var array
152
     */
153
    private static $defaultStyleValues = [
154
        'fg' => 'white',
155
        'bg' => 'blue',
156
        'width' => 100,
157
        'paddingTopBottom' => 1,
158
        'paddingLeftRight' => 2,
159
        'margin' => 2,
160
        'selectedMarker' => '●',
161
        'unselectedMarker' => '○',
162
        'itemExtra' => '✔',
163
        'displaysExtra' => false,
164
        'titleSeparator' => '=',
165
        'borderTopWidth' => 0,
166
        'borderRightWidth' => 0,
167
        'borderBottomWidth' => 0,
168
        'borderLeftWidth' => 0,
169
        'borderColour' => 'white',
170
        'borderColourFallback' => null,
171
        'marginAuto' => false,
172
    ];
173
174
    public static function getDefaultStyleValues() : array
175
    {
176
        return static::$defaultStyleValues;
177
    }
178
179
    /**
180
     * @var array
181
     */
182
    private static $availableForegroundColors = array(
183
        'black'   => 30,
184
        'red'     => 31,
185
        'green'   => 32,
186
        'yellow'  => 33,
187
        'blue'    => 34,
188
        'magenta' => 35,
189
        'cyan'    => 36,
190
        'white'   => 37,
191
        'default' => 39,
192
    );
193
194
    /**
195
     * @var array
196
     */
197
    private static $availableBackgroundColors = array(
198
        'black'   => 40,
199
        'red'     => 41,
200
        'green'   => 42,
201
        'yellow'  => 43,
202
        'blue'    => 44,
203
        'magenta' => 45,
204
        'cyan'    => 46,
205
        'white'   => 47,
206
        'default' => 49,
207
    );
208
209
    /**
210
     * @var array
211
     */
212
    private static $availableOptions = array(
213
        'bold'       => array('set' => 1, 'unset' => 22),
214
        'dim'        => array('set' => 2, 'unset' => 22),
215
        'underscore' => array('set' => 4, 'unset' => 24),
216
        'blink'      => array('set' => 5, 'unset' => 25),
217
        'reverse'    => array('set' => 7, 'unset' => 27),
218
        'conceal'    => array('set' => 8, 'unset' => 28)
219
    );
220
221
    /**
222
     * Initialise style
223
     */
224
    public function __construct(Terminal $terminal = null)
225
    {
226
        $this->terminal = $terminal ?: TerminalFactory::fromSystem();
227
228
        $this->fg = static::$defaultStyleValues['fg'];
229
        $this->bg = static::$defaultStyleValues['bg'];
230
231
        $this->generateColoursSetCode();
232
233
        $this->setWidth(static::$defaultStyleValues['width']);
234
        $this->setPaddingTopBottom(static::$defaultStyleValues['paddingTopBottom']);
235
        $this->setPaddingLeftRight(static::$defaultStyleValues['paddingLeftRight']);
236
        $this->setMargin(static::$defaultStyleValues['margin']);
237
        $this->setSelectedMarker(static::$defaultStyleValues['selectedMarker']);
238
        $this->setUnselectedMarker(static::$defaultStyleValues['unselectedMarker']);
239
        $this->setItemExtra(static::$defaultStyleValues['itemExtra']);
240
        $this->setDisplaysExtra(static::$defaultStyleValues['displaysExtra']);
241
        $this->setTitleSeparator(static::$defaultStyleValues['titleSeparator']);
242
        $this->setBorderTopWidth(static::$defaultStyleValues['borderTopWidth']);
243
        $this->setBorderRightWidth(static::$defaultStyleValues['borderRightWidth']);
244
        $this->setBorderBottomWidth(static::$defaultStyleValues['borderBottomWidth']);
245
        $this->setBorderLeftWidth(static::$defaultStyleValues['borderLeftWidth']);
246
        $this->setBorderColour(
247
            static::$defaultStyleValues['borderColour'],
248
            static::$defaultStyleValues['borderColourFallback']
249
        );
250
    }
251
252
    public function getDisabledItemText(string $text) : string
253
    {
254
        return sprintf(
255
            "\033[%sm%s\033[%sm",
256
            self::$availableOptions['dim']['set'],
257
            $text,
258
            self::$availableOptions['dim']['unset']
259
        );
260
    }
261
262
    /**
263
     * Generates the ansi escape sequence to set the colours
264
     */
265
    private function generateColoursSetCode() : void
266
    {
267
        if (!ctype_digit($this->fg)) {
268
            $fgCode = self::$availableForegroundColors[$this->fg];
269
        } else {
270
            $fgCode = sprintf("38;5;%s", $this->fg);
271
        }
272
273
        if (!ctype_digit($this->bg)) {
274
            $bgCode = self::$availableBackgroundColors[$this->bg];
275
        } else {
276
            $bgCode = sprintf("48;5;%s", $this->bg);
277
        }
278
279
        $this->coloursSetCode = sprintf("\033[%s;%sm", $fgCode, $bgCode);
280
    }
281
282
    /**
283
     * Get the colour code for Bg and Fg
284
     */
285
    public function getColoursSetCode() : string
286
    {
287
        return $this->coloursSetCode;
288
    }
289
290
    /**
291
     * Get the inverted escape sequence (used for selected elements)
292
     */
293
    public function getInvertedColoursSetCode() : string
294
    {
295
        return $this->invertedColoursSetCode;
296
    }
297
298
    /**
299
     * Get the inverted escape sequence (used for selected elements)
300
     */
301
    public function getInvertedColoursUnsetCode() : string
302
    {
303
        return $this->invertedColoursUnsetCode;
304
    }
305
306
    /**
307
     * Get the escape sequence used to reset colours to default
308
     */
309
    public function getColoursResetCode() : string
310
    {
311
        return $this->coloursResetCode;
312
    }
313
314
    /**
315
     * Calculate the contents width
316
     */
317
    protected function calculateContentWidth() : void
318
    {
319
        $this->contentWidth = $this->width
320
            - ($this->paddingLeftRight * 2)
321
            - ($this->borderRightWidth + $this->borderLeftWidth);
322
323
        if ($this->contentWidth < 0) {
324
            $this->contentWidth = 0;
325
        }
326
    }
327
328
    public function getFg()
329
    {
330
        return $this->fg;
331
    }
332
333
    public function setFg(string $fg, string $fallback = null) : self
334
    {
335
        $this->fg = ColourUtil::validateColour(
336
            $this->terminal,
337
            $fg,
338
            $fallback
339
        );
340
        $this->generateColoursSetCode();
341
342
        return $this;
343
    }
344
345
    public function getBg()
346
    {
347
        return $this->bg;
348
    }
349
350
    public function setBg(string $bg, string $fallback = null) : self
351
    {
352
        $this->bg = ColourUtil::validateColour(
353
            $this->terminal,
354
            $bg,
355
            $fallback
356
        );
357
358
        $this->generateColoursSetCode();
359
        $this->generatePaddingTopBottomRows();
360
361
        return $this;
362
    }
363
364
    public function getWidth() : int
365
    {
366
        return $this->width;
367
    }
368
369
    public function setWidth(int $width) : self
370
    {
371
        Assertion::greaterOrEqualThan($width, 0);
372
373
        if ($width >= $this->terminal->getWidth()) {
374
            $width = $this->terminal->getWidth();
375
        }
376
377
        $this->width = $width;
378
        if ($this->marginAuto) {
379
            $this->setMarginAuto();
380
        }
381
382
        $this->calculateContentWidth();
383
        $this->generateBorderRows();
384
        $this->generatePaddingTopBottomRows();
385
386
        return $this;
387
    }
388
389
    public function getPaddingTopBottom() : int
390
    {
391
        return $this->paddingTopBottom;
392
    }
393
394
    public function getPaddingLeftRight() : int
395
    {
396
        return $this->paddingLeftRight;
397
    }
398
399
    private function generatePaddingTopBottomRows() : void
400
    {
401
        if ($this->borderLeftWidth || $this->borderRightWidth) {
402
            $borderColour = $this->getBorderColourCode();
403
        } else {
404
            $borderColour = '';
405
        }
406
407
        $paddingRow = sprintf(
408
            "%s%s%s%s%s%s%s%s%s%s\n",
409
            str_repeat(' ', $this->margin),
410
            $borderColour,
411
            str_repeat(' ', $this->borderLeftWidth),
412
            $this->getColoursSetCode(),
413
            str_repeat(' ', $this->paddingLeftRight),
414
            str_repeat(' ', $this->contentWidth),
415
            str_repeat(' ', $this->paddingLeftRight),
416
            $borderColour,
417
            str_repeat(' ', $this->borderRightWidth),
418
            $this->coloursResetCode
419
        );
420
421
        $this->paddingTopBottomRows = array_fill(0, $this->paddingTopBottom, $paddingRow);
422
    }
423
424
    public function getPaddingTopBottomRows() : array
425
    {
426
        return $this->paddingTopBottomRows;
427
    }
428
429
    public function setPadding(int $topBottom, int $leftRight = null) : self
430
    {
431
        if ($leftRight === null) {
432
            $leftRight = $topBottom;
433
        }
434
435
        $this->setPaddingTopBottom($topBottom);
436
        $this->setPaddingLeftRight($leftRight);
437
438
        $this->calculateContentWidth();
439
        $this->generatePaddingTopBottomRows();
440
441
        return $this;
442
    }
443
444
    public function setPaddingTopBottom(int $topBottom) : self
445
    {
446
        Assertion::greaterOrEqualThan($topBottom, 0);
447
        $this->paddingTopBottom = $topBottom;
448
449
        $this->generatePaddingTopBottomRows();
450
451
        return $this;
452
    }
453
454
    public function setPaddingLeftRight(int $leftRight) : self
455
    {
456
        Assertion::greaterOrEqualThan($leftRight, 0);
457
        $this->paddingLeftRight = $leftRight;
458
459
        $this->calculateContentWidth();
460
        $this->generatePaddingTopBottomRows();
461
462
        return $this;
463
    }
464
465
    public function getMargin() : int
466
    {
467
        return $this->margin;
468
    }
469
470
    public function setMarginAuto() : self
471
    {
472
        $this->marginAuto = true;
473
        $this->margin = floor(($this->terminal->getWidth() - $this->width) / 2);
0 ignored issues
show
Documentation Bug introduced by
The property $margin was declared of type integer, but floor(($this->terminal->...() - $this->width) / 2) is of type double. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
474
475
        $this->generateBorderRows();
476
        $this->generatePaddingTopBottomRows();
477
478
        return $this;
479
    }
480
481
    public function setMargin(int $margin) : self
482
    {
483
        Assertion::greaterOrEqualThan($margin, 0);
484
485
        $this->marginAuto = false;
486
        $this->margin = $margin;
487
488
        $this->generateBorderRows();
489
        $this->generatePaddingTopBottomRows();
490
491
        return $this;
492
    }
493
494
    public function getContentWidth() : int
495
    {
496
        return $this->contentWidth;
497
    }
498
499
    /**
500
     * Get padding for right had side of content
501
     */
502
    public function getRightHandPadding(int $contentLength) : int
503
    {
504
        $rightPadding = $this->getContentWidth() - $contentLength + $this->getPaddingLeftRight();
505
506
        if ($rightPadding < 0) {
507
            $rightPadding = 0;
508
        }
509
510
        return $rightPadding;
511
    }
512
513
    public function getSelectedMarker() : string
514
    {
515
        return $this->selectedMarker;
516
    }
517
518
    public function setSelectedMarker(string $marker) : self
519
    {
520
        $this->selectedMarker = mb_substr($marker, 0, 1);
521
522
        return $this;
523
    }
524
525
    public function getUnselectedMarker() : string
526
    {
527
        return $this->unselectedMarker;
528
    }
529
530
    public function setUnselectedMarker(string $marker) : self
531
    {
532
        $this->unselectedMarker = mb_substr($marker, 0, 1);
533
534
        return $this;
535
    }
536
537
    /**
538
     * Get the correct marker for the item
539
     */
540
    public function getMarker(bool $selected) : string
541
    {
542
        return $selected ? $this->selectedMarker : $this->unselectedMarker;
543
    }
544
545
    public function setItemExtra(string $itemExtra) : self
546
    {
547
        $this->itemExtra = $itemExtra;
548
549
        return $this;
550
    }
551
552
    public function getItemExtra() : string
553
    {
554
        return $this->itemExtra;
555
    }
556
557
    public function getDisplaysExtra() : bool
558
    {
559
        return $this->displaysExtra;
560
    }
561
562
    public function setDisplaysExtra(bool $displaysExtra) : self
563
    {
564
        $this->displaysExtra = $displaysExtra;
565
566
        return $this;
567
    }
568
569
    public function getTitleSeparator() : string
570
    {
571
        return $this->titleSeparator;
572
    }
573
574
    public function setTitleSeparator(string $actionSeparator) : self
575
    {
576
        $this->titleSeparator = $actionSeparator;
577
578
        return $this;
579
    }
580
581
    private function generateBorderRows() : void
582
    {
583
        $borderRow = sprintf(
584
            "%s%s%s%s\n",
585
            str_repeat(' ', $this->margin),
586
            $this->getBorderColourCode(),
587
            str_repeat(' ', $this->width),
588
            $this->coloursResetCode
589
        );
590
591
        $this->borderTopRows = array_fill(0, $this->borderTopWidth, $borderRow);
592
        $this->borderBottomRows = array_fill(0, $this->borderBottomWidth, $borderRow);
593
    }
594
595
    public function getBorderTopRows() : array
596
    {
597
        return $this->borderTopRows;
598
    }
599
600
    public function getBorderBottomRows() : array
601
    {
602
        return $this->borderBottomRows;
603
    }
604
605
    /**
606
     * Shorthand function to set all borders values at once
607
     */
608
    public function setBorder(
609
        int $topWidth,
610
        $rightWidth = null,
611
        $bottomWidth = null,
612
        $leftWidth = null,
613
        string $colour = null
614
    ) : self {
615
        if (!is_int($rightWidth)) {
616
            $colour = $rightWidth;
617
            $rightWidth = $bottomWidth = $leftWidth = $topWidth;
618
        } elseif (!is_int($bottomWidth)) {
619
            $colour = $bottomWidth;
620
            $bottomWidth = $topWidth;
621
            $leftWidth = $rightWidth;
622
        } elseif (!is_int($leftWidth)) {
623
            $colour = $leftWidth;
624
            $leftWidth = $rightWidth;
625
        }
626
627
        $this->borderTopWidth = $topWidth;
628
        $this->borderRightWidth = $rightWidth;
629
        $this->borderBottomWidth = $bottomWidth;
630
        $this->borderLeftWidth = $leftWidth;
631
632
        if (is_string($colour)) {
633
            $this->setBorderColour($colour);
634
        } elseif ($colour !== null) {
635
            throw new \InvalidArgumentException('Invalid colour');
636
        }
637
638
        $this->calculateContentWidth();
639
        $this->generateBorderRows();
640
        $this->generatePaddingTopBottomRows();
641
642
        return $this;
643
    }
644
645
    public function setBorderTopWidth(int $width) : self
646
    {
647
        $this->borderTopWidth = $width;
648
649
        $this->generateBorderRows();
650
651
        return $this;
652
    }
653
654
    public function setBorderRightWidth(int $width) : self
655
    {
656
        $this->borderRightWidth = $width;
657
        $this->calculateContentWidth();
658
659
        $this->generatePaddingTopBottomRows();
660
661
        return $this;
662
    }
663
664
    public function setBorderBottomWidth(int $width) : self
665
    {
666
        $this->borderBottomWidth = $width;
667
668
        $this->generateBorderRows();
669
670
        return $this;
671
    }
672
673
    public function setBorderLeftWidth(int $width) : self
674
    {
675
        $this->borderLeftWidth = $width;
676
        $this->calculateContentWidth();
677
678
        $this->generatePaddingTopBottomRows();
679
680
        return $this;
681
    }
682
683
    public function setBorderColour(string $colour, $fallback = null) : self
684
    {
685
        $this->borderColour = ColourUtil::validateColour(
686
            $this->terminal,
687
            $colour,
688
            $fallback
689
        );
690
691
        $this->generateBorderRows();
692
        $this->generatePaddingTopBottomRows();
693
694
        return $this;
695
    }
696
697
    public function getBorderTopWidth() : int
698
    {
699
        return $this->borderTopWidth;
700
    }
701
702
    public function getBorderRightWidth() : int
703
    {
704
        return $this->borderRightWidth;
705
    }
706
707
    public function getBorderBottomWidth() : int
708
    {
709
        return $this->borderBottomWidth;
710
    }
711
712
    public function getBorderLeftWidth() : int
713
    {
714
        return $this->borderLeftWidth;
715
    }
716
717
    public function getBorderColour() : string
718
    {
719
        return $this->borderColour;
720
    }
721
722
    public function getBorderColourCode() : string
723
    {
724
        if (!ctype_digit($this->borderColour)) {
725
            $borderColourCode = self::$availableBackgroundColors[$this->borderColour];
726
        } else {
727
            $borderColourCode = sprintf("48;5;%s", $this->borderColour);
728
        }
729
730
        return sprintf("\033[%sm", $borderColourCode);
731
    }
732
}
733