Completed
Pull Request — master (#222)
by Aydin
01:53
created

MenuStyle::getDebugString()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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