MenuStyle::getBg()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 0
dl 0
loc 3
rs 10
1
<?php
2
3
namespace PhpSchool\CliMenu;
4
5
use PhpSchool\CliMenu\Exception\CannotShrinkMenuException;
6
use PhpSchool\CliMenu\Terminal\TerminalFactory;
7
use PhpSchool\CliMenu\Util\ColourUtil;
8
use PhpSchool\CliMenu\Util\StringUtil as s;
9
use PhpSchool\Terminal\Terminal;
10
use Assert\Assertion;
11
12
//TODO: B/W fallback
13
14
/**
15
 * @author Michael Woodward <[email protected]>
16
 */
17
class MenuStyle
18
{
19
    /**
20
     * @var Terminal
21
     */
22
    protected $terminal;
23
24
    /**
25
     * @var string
26
     */
27
    protected $fg;
28
29
    /**
30
     * @var string
31
     */
32
    protected $bg;
33
34
    /**
35
     * The width of the menu. Including borders and padding.
36
     * Does not include margin.
37
     *
38
     * May not be the value that was requested in the
39
     * circumstance that the terminal is smaller then the
40
     * requested width.
41
     *
42
     * @var int
43
     */
44
    protected $width;
45
46
    /**
47
     * In case the requested width is wider than the terminal
48
     * then we shrink the width to fit the terminal. We keep
49
     * the requested size in case the margins are changed and
50
     * we need to recalculate the width.
51
     *
52
     * @var int
53
     */
54
    private $requestedWidth;
55
56
    /**
57
     * @var int
58
     */
59
    protected $margin = 0;
60
61
    /**
62
     * @var int
63
     */
64
    protected $paddingTopBottom;
65
66
    /**
67
     * @var int
68
     */
69
    protected $paddingLeftRight;
70
71
    /**
72
     * @var array
73
     */
74
    private $paddingTopBottomRows = [];
75
76
    /**
77
     * @var int
78
     */
79
    protected $contentWidth;
80
81
    /**
82
     * @var string
83
     */
84
    private $itemExtra;
85
86
    /**
87
     * @var bool
88
     */
89
    private $displaysExtra;
90
91
    /**
92
     * @var string
93
     */
94
    private $titleSeparator;
95
96
    /**
97
     * @var string
98
     */
99
    private $coloursSetCode;
100
101
    /**
102
     * @var string
103
     */
104
    private $invertedColoursSetCode = "\033[7m";
105
106
    /**
107
     * @var string
108
     */
109
    private $invertedColoursUnsetCode = "\033[27m";
110
111
    /**
112
     * @var string
113
     */
114
    private $coloursResetCode = "\033[0m";
115
116
    /**
117
     * @var int
118
     */
119
    private $borderTopWidth;
120
121
    /**
122
     * @var int
123
     */
124
    private $borderRightWidth;
125
126
    /**
127
     * @var int
128
     */
129
    private $borderBottomWidth;
130
131
    /**
132
     * @var int
133
     */
134
    private $borderLeftWidth;
135
136
    /**
137
     * @var string
138
     */
139
    private $borderColour = 'white';
140
141
    /**
142
     * @var array
143
     */
144
    private $borderTopRows = [];
145
146
    /**
147
     * @var array
148
     */
149
    private $borderBottomRows = [];
150
151
    /**
152
     * @var bool
153
     */
154
    private $marginAuto = false;
155
156
    /**
157
     * @var bool
158
     */
159
    private $debugMode = false;
160
161
    /**
162
     * Default Values
163
     *
164
     * @var array
165
     */
166
    private static $defaultStyleValues = [
167
        'fg' => 'white',
168
        'bg' => 'blue',
169
        'width' => 100,
170
        'paddingTopBottom' => 1,
171
        'paddingLeftRight' => 2,
172
        'margin' => 2,
173
        'itemExtra' => '✔',
174
        'displaysExtra' => false,
175
        'titleSeparator' => '=',
176
        'borderTopWidth' => 0,
177
        'borderRightWidth' => 0,
178
        'borderBottomWidth' => 0,
179
        'borderLeftWidth' => 0,
180
        'borderColour' => 'white',
181
        'marginAuto' => false,
182
    ];
183
184
    /**
185
     * @var array
186
     */
187
    private static $availableForegroundColors = [
188
        'black'   => 30,
189
        'red'     => 31,
190
        'green'   => 32,
191
        'yellow'  => 33,
192
        'blue'    => 34,
193
        'magenta' => 35,
194
        'cyan'    => 36,
195
        'white'   => 37,
196
        'default' => 39,
197
    ];
198
199
    /**
200
     * @var array
201
     */
202
    private static $availableBackgroundColors = [
203
        'black'   => 40,
204
        'red'     => 41,
205
        'green'   => 42,
206
        'yellow'  => 43,
207
        'blue'    => 44,
208
        'magenta' => 45,
209
        'cyan'    => 46,
210
        'white'   => 47,
211
        'default' => 49,
212
    ];
213
214
    /**
215
     * @var array
216
     */
217
    private static $availableOptions = [
218
        'bold'       => ['set' => 1, 'unset' => 22],
219
        'dim'        => ['set' => 2, 'unset' => 22],
220
        'underscore' => ['set' => 4, 'unset' => 24],
221
        'blink'      => ['set' => 5, 'unset' => 25],
222
        'reverse'    => ['set' => 7, 'unset' => 27],
223
        'conceal'    => ['set' => 8, 'unset' => 28]
224
    ];
225
226
    /**
227
     * Initialise style
228
     */
229
    public function __construct(Terminal $terminal = null)
230
    {
231
        $this->terminal = $terminal ?: TerminalFactory::fromSystem();
232
233
        $this->fg = self::$defaultStyleValues['fg'];
234
        $this->bg = self::$defaultStyleValues['bg'];
235
236
        $this->generateColoursSetCode();
237
238
        $this->setWidth(self::$defaultStyleValues['width']);
239
        $this->setPaddingTopBottom(self::$defaultStyleValues['paddingTopBottom']);
240
        $this->setPaddingLeftRight(self::$defaultStyleValues['paddingLeftRight']);
241
        $this->setMargin(self::$defaultStyleValues['margin']);
242
        $this->setItemExtra(self::$defaultStyleValues['itemExtra']);
243
        $this->setDisplaysExtra(self::$defaultStyleValues['displaysExtra']);
244
        $this->setTitleSeparator(self::$defaultStyleValues['titleSeparator']);
245
        $this->setBorderTopWidth(self::$defaultStyleValues['borderTopWidth']);
246
        $this->setBorderRightWidth(self::$defaultStyleValues['borderRightWidth']);
247
        $this->setBorderBottomWidth(self::$defaultStyleValues['borderBottomWidth']);
248
        $this->setBorderLeftWidth(self::$defaultStyleValues['borderLeftWidth']);
249
        $this->setBorderColour(self::$defaultStyleValues['borderColour']);
250
    }
251
252
    public function hasChangedFromDefaults() : bool
253
    {
254
        $currentValues = [
255
            $this->fg,
256
            $this->bg,
257
            $this->width,
258
            $this->paddingTopBottom,
259
            $this->paddingLeftRight,
260
            $this->margin,
261
            $this->itemExtra,
262
            $this->displaysExtra,
263
            $this->titleSeparator,
264
            $this->borderTopWidth,
265
            $this->borderRightWidth,
266
            $this->borderBottomWidth,
267
            $this->borderLeftWidth,
268
            $this->borderColour,
269
            $this->marginAuto,
270
        ];
271
272
        $defaultStyleValues = self::$defaultStyleValues;
273
        if ($this->width !== $this->requestedWidth) {
274
            $defaultStyleValues['width'] = $this->width;
275
        }
276
277
        return $currentValues !== array_values($defaultStyleValues);
278
    }
279
280
    public function getDisabledItemText(string $text) : string
281
    {
282
        return sprintf(
283
            "\033[%sm%s\033[%sm",
284
            self::$availableOptions['dim']['set'],
285
            $text,
286
            self::$availableOptions['dim']['unset']
287
        );
288
    }
289
290
    /**
291
     * Generates the ansi escape sequence to set the colours
292
     */
293
    private function generateColoursSetCode() : void
294
    {
295
        if (!ctype_digit($this->fg)) {
296
            $fgCode = self::$availableForegroundColors[$this->fg];
297
        } else {
298
            $fgCode = sprintf("38;5;%s", $this->fg);
299
        }
300
301
        if (!ctype_digit($this->bg)) {
302
            $bgCode = self::$availableBackgroundColors[$this->bg];
303
        } else {
304
            $bgCode = sprintf("48;5;%s", $this->bg);
305
        }
306
307
        $this->coloursSetCode = sprintf("\033[%s;%sm", $fgCode, $bgCode);
308
    }
309
310
    /**
311
     * Get the colour code for Bg and Fg
312
     */
313
    public function getColoursSetCode() : string
314
    {
315
        return $this->coloursSetCode;
316
    }
317
318
    /**
319
     * Get the inverted escape sequence (used for selected elements)
320
     */
321
    public function getInvertedColoursSetCode() : string
322
    {
323
        return $this->invertedColoursSetCode;
324
    }
325
326
    /**
327
     * Get the inverted escape sequence (used for selected elements)
328
     */
329
    public function getInvertedColoursUnsetCode() : string
330
    {
331
        return $this->invertedColoursUnsetCode;
332
    }
333
334
    /**
335
     * Get the escape sequence used to reset colours to default
336
     */
337
    public function getColoursResetCode() : string
338
    {
339
        return $this->coloursResetCode;
340
    }
341
342
    /**
343
     * Calculate the contents width
344
     *
345
     * The content width is menu width minus borders and padding.
346
     */
347
    protected function calculateContentWidth() : void
348
    {
349
        $this->contentWidth = $this->width
350
            - ($this->paddingLeftRight * 2)
351
            - ($this->borderRightWidth + $this->borderLeftWidth);
352
353
        if ($this->contentWidth < 0) {
354
            $this->contentWidth = 0;
355
        }
356
    }
357
358
    public function getFg() : string
359
    {
360
        return $this->fg;
361
    }
362
363
    public function setFg(string $fg, string $fallback = null) : self
364
    {
365
        $this->fg = ColourUtil::validateColour(
366
            $this->terminal,
367
            $fg,
368
            $fallback
369
        );
370
        $this->generateColoursSetCode();
371
372
        return $this;
373
    }
374
375
    public function getBg() : string
376
    {
377
        return $this->bg;
378
    }
379
380
    public function setBg(string $bg, string $fallback = null) : self
381
    {
382
        $this->bg = ColourUtil::validateColour(
383
            $this->terminal,
384
            $bg,
385
            $fallback
386
        );
387
388
        $this->generateColoursSetCode();
389
        $this->generatePaddingTopBottomRows();
390
391
        return $this;
392
    }
393
394
    public function getWidth() : int
395
    {
396
        return $this->width;
397
    }
398
399
    public function setWidth(int $width) : self
400
    {
401
        Assertion::greaterOrEqualThan($width, 0);
402
403
        $this->requestedWidth = $width;
404
405
        $this->width = $this->maybeShrinkWidth($this->marginAuto ? 0 : $this->margin, $width);
406
407
        if ($this->marginAuto) {
408
            $this->calculateMarginAuto($this->width);
409
        }
410
411
        $this->calculateContentWidth();
412
        $this->generateBorderRows();
413
        $this->generatePaddingTopBottomRows();
414
415
        return $this;
416
    }
417
418
    private function maybeShrinkWidth(int $margin, int $width) : int
419
    {
420
        if ($width + ($margin * 2) >= $this->terminal->getWidth()) {
421
            $width = $this->terminal->getWidth() - ($margin * 2);
422
423
            if ($width <= 0) {
424
                throw CannotShrinkMenuException::fromMarginAndTerminalWidth($margin, $this->terminal->getWidth());
425
            }
426
        }
427
428
        return $width;
429
    }
430
431
    public function getPaddingTopBottom() : int
432
    {
433
        return $this->paddingTopBottom;
434
    }
435
436
    public function getPaddingLeftRight() : int
437
    {
438
        return $this->paddingLeftRight;
439
    }
440
441
    private function generatePaddingTopBottomRows() : void
442
    {
443
        if ($this->borderLeftWidth || $this->borderRightWidth) {
444
            $borderColour = $this->getBorderColourCode();
445
        } else {
446
            $borderColour = '';
447
        }
448
449
        $paddingRow = sprintf(
450
            "%s%s%s%s%s%s%s%s%s%s\n",
451
            $this->debugMode ? $this->getDebugString($this->margin) : str_repeat(' ', $this->margin),
452
            $borderColour,
453
            str_repeat(' ', $this->borderLeftWidth),
454
            $this->getColoursSetCode(),
455
            str_repeat(' ', $this->paddingLeftRight),
456
            str_repeat(' ', $this->contentWidth),
457
            str_repeat(' ', $this->paddingLeftRight),
458
            $borderColour,
459
            str_repeat(' ', $this->borderRightWidth),
460
            $this->coloursResetCode
461
        );
462
463
464
        if ($this->debugMode && s::length($paddingRow) <= $this->terminal->getWidth()) {
465
            $paddingRow = substr_replace(
466
                $paddingRow,
467
                sprintf("%s\n", $this->getDebugString($this->terminal->getWidth() - (s::length($paddingRow) - 1))),
468
                -1
469
            );
470
        }
471
472
        $this->paddingTopBottomRows = array_fill(0, $this->paddingTopBottom, $paddingRow);
473
    }
474
475
    /**
476
     * @return array
477
     */
478
    public function getPaddingTopBottomRows() : array
479
    {
480
        return $this->paddingTopBottomRows;
481
    }
482
483
    public function setPadding(int $topBottom, int $leftRight = null) : self
484
    {
485
        if ($leftRight === null) {
486
            $leftRight = $topBottom;
487
        }
488
489
        $this->setPaddingTopBottom($topBottom);
490
        $this->setPaddingLeftRight($leftRight);
491
492
        $this->calculateContentWidth();
493
        $this->generatePaddingTopBottomRows();
494
495
        return $this;
496
    }
497
498
    public function setPaddingTopBottom(int $topBottom) : self
499
    {
500
        Assertion::greaterOrEqualThan($topBottom, 0);
501
        $this->paddingTopBottom = $topBottom;
502
503
        $this->generatePaddingTopBottomRows();
504
505
        return $this;
506
    }
507
508
    public function setPaddingLeftRight(int $leftRight) : self
509
    {
510
        Assertion::greaterOrEqualThan($leftRight, 0);
511
        $this->paddingLeftRight = $leftRight;
512
513
        $this->calculateContentWidth();
514
        $this->generatePaddingTopBottomRows();
515
516
        return $this;
517
    }
518
519
    public function getMargin() : int
520
    {
521
        return $this->margin;
522
    }
523
524
    public function setMarginAuto() : self
525
    {
526
        $this->marginAuto = true;
527
        $this->margin = 0;
528
529
        $this->setWidth($this->requestedWidth);
530
531
        return $this;
532
    }
533
534
    private function calculateMarginAuto(int $width) : void
535
    {
536
        $this->margin = (int) floor(($this->terminal->getWidth() - ($width)) / 2);
537
    }
538
539
    public function setMargin(int $margin) : self
540
    {
541
        Assertion::greaterOrEqualThan($margin, 0);
542
543
        $this->marginAuto = false;
544
        $this->margin = $margin;
545
546
        //margin + width may now exceed terminal size
547
        //so set width again to trigger width check + maybe resize
548
        $this->setWidth($this->requestedWidth);
549
550
        return $this;
551
    }
552
553
    public function getContentWidth() : int
554
    {
555
        return $this->contentWidth;
556
    }
557
558
    /**
559
     * Get padding for right had side of content
560
     */
561
    public function getRightHandPadding(int $contentLength) : int
562
    {
563
        $rightPadding = $this->getContentWidth() - $contentLength + $this->getPaddingLeftRight();
564
565
        if ($rightPadding < 0) {
566
            $rightPadding = 0;
567
        }
568
569
        return $rightPadding;
570
    }
571
572
    public function setItemExtra(string $itemExtra) : self
573
    {
574
        $this->itemExtra = $itemExtra;
575
576
        return $this;
577
    }
578
579
    public function getItemExtra() : string
580
    {
581
        return $this->itemExtra;
582
    }
583
584
    public function getDisplaysExtra() : bool
585
    {
586
        return $this->displaysExtra;
587
    }
588
589
    public function setDisplaysExtra(bool $displaysExtra) : self
590
    {
591
        $this->displaysExtra = $displaysExtra;
592
593
        return $this;
594
    }
595
596
    public function getTitleSeparator() : string
597
    {
598
        return $this->titleSeparator;
599
    }
600
601
    public function setTitleSeparator(string $actionSeparator) : self
602
    {
603
        $this->titleSeparator = $actionSeparator;
604
605
        return $this;
606
    }
607
608
    private function generateBorderRows() : void
609
    {
610
        $borderRow = sprintf(
611
            "%s%s%s%s\n",
612
            $this->debugMode ? $this->getDebugString($this->margin) : str_repeat(' ', $this->margin),
613
            $this->getBorderColourCode(),
614
            str_repeat(' ', $this->width),
615
            $this->getColoursResetCode()
616
        );
617
618
        if ($this->debugMode && s::length($borderRow) <= $this->terminal->getWidth()) {
619
            $borderRow = substr_replace(
620
                $borderRow,
621
                sprintf("%s\n", $this->getDebugString($this->terminal->getWidth() - (s::length($borderRow) - 1))),
622
                -1
623
            );
624
        }
625
626
        $this->borderTopRows = array_fill(0, $this->borderTopWidth, $borderRow);
627
        $this->borderBottomRows = array_fill(0, $this->borderBottomWidth, $borderRow);
628
    }
629
630
    /**
631
     * @return array
632
     */
633
    public function getBorderTopRows() : array
634
    {
635
        return $this->borderTopRows;
636
    }
637
638
    /**
639
     * @return array
640
     */
641
    public function getBorderBottomRows() : array
642
    {
643
        return $this->borderBottomRows;
644
    }
645
646
    /**
647
     * @param int|string|null $rightWidth
648
     * @param int|string|null $bottomWidth
649
     * @param int|string|null $leftWidth
650
     *
651
     * Shorthand function to set all borders values at once
652
     */
653
    public function setBorder(
654
        int $topWidth,
655
        $rightWidth = null,
656
        $bottomWidth = null,
657
        $leftWidth = null,
658
        string $colour = null
659
    ) : self {
660
        if (!is_int($rightWidth)) {
661
            $colour = $rightWidth;
662
            $rightWidth = $bottomWidth = $leftWidth = $topWidth;
663
        } elseif (!is_int($bottomWidth)) {
664
            $colour = $bottomWidth;
665
            $bottomWidth = $topWidth;
666
            $leftWidth = $rightWidth;
667
        } elseif (!is_int($leftWidth)) {
668
            $colour = $leftWidth;
669
            $leftWidth = $rightWidth;
670
        }
671
672
        $this->borderTopWidth = $topWidth;
673
        $this->borderRightWidth = $rightWidth;
674
        $this->borderBottomWidth = $bottomWidth;
675
        $this->borderLeftWidth = $leftWidth;
676
677
        if (is_string($colour)) {
678
            $this->setBorderColour($colour);
679
        }
680
681
        $this->calculateContentWidth();
682
        $this->generateBorderRows();
683
        $this->generatePaddingTopBottomRows();
684
685
        return $this;
686
    }
687
688
    public function setBorderTopWidth(int $width) : self
689
    {
690
        $this->borderTopWidth = $width;
691
692
        $this->generateBorderRows();
693
694
        return $this;
695
    }
696
697
    public function setBorderRightWidth(int $width) : self
698
    {
699
        $this->borderRightWidth = $width;
700
        $this->calculateContentWidth();
701
702
        $this->generatePaddingTopBottomRows();
703
704
        return $this;
705
    }
706
707
    public function setBorderBottomWidth(int $width) : self
708
    {
709
        $this->borderBottomWidth = $width;
710
711
        $this->generateBorderRows();
712
713
        return $this;
714
    }
715
716
    public function setBorderLeftWidth(int $width) : self
717
    {
718
        $this->borderLeftWidth = $width;
719
        $this->calculateContentWidth();
720
721
        $this->generatePaddingTopBottomRows();
722
723
        return $this;
724
    }
725
726
    public function setBorderColour(string $colour, string $fallback = null) : self
727
    {
728
        $this->borderColour = ColourUtil::validateColour(
729
            $this->terminal,
730
            $colour,
731
            $fallback
732
        );
733
734
        $this->generateBorderRows();
735
        $this->generatePaddingTopBottomRows();
736
737
        return $this;
738
    }
739
740
    public function getBorderTopWidth() : int
741
    {
742
        return $this->borderTopWidth;
743
    }
744
745
    public function getBorderRightWidth() : int
746
    {
747
        return $this->borderRightWidth;
748
    }
749
750
    public function getBorderBottomWidth() : int
751
    {
752
        return $this->borderBottomWidth;
753
    }
754
755
    public function getBorderLeftWidth() : int
756
    {
757
        return $this->borderLeftWidth;
758
    }
759
760
    public function getBorderColour() : string
761
    {
762
        return $this->borderColour;
763
    }
764
765
    public function getBorderColourCode() : string
766
    {
767
        if (!ctype_digit($this->borderColour)) {
768
            $borderColourCode = self::$availableBackgroundColors[$this->borderColour];
769
        } else {
770
            $borderColourCode = sprintf("48;5;%s", $this->borderColour);
771
        }
772
773
        return sprintf("\033[%sm", $borderColourCode);
774
    }
775
776
    /**
777
     * Get a string of given length consisting of 0-9
778
     * eg $length = 15 : 012345678901234
779
     */
780
    private function getDebugString(int $length) : string
781
    {
782
        $nums = [];
783
        for ($i = 0, $j = 0; $i < $length; $i++, $j++) {
784
            if ($j === 10) {
785
                $j = 0;
786
            }
787
788
            $nums[] = $j;
789
        }
790
791
        return implode('', $nums);
792
    }
793
}
794