Completed
Push — master ( 1316f3...b7872d )
by Aydin
26s queued 12s
created

MenuStyle::generatePaddingTopBottomRows()   B

Complexity

Conditions 6
Paths 4

Size

Total Lines 32
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

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