Completed
Push — master ( b7872d...0283b1 )
by Aydin
02:24 queued 10s
created

MenuStyle::setBorder()   A

Complexity

Conditions 5
Paths 8

Size

Total Lines 33
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 9
Bugs 1 Features 0
Metric Value
cc 5
eloc 20
c 9
b 1
f 0
nc 8
nop 5
dl 0
loc 33
rs 9.2888
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() : string
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() : string
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
    /**
471
     * @return array
472
     */
473
    public function getPaddingTopBottomRows() : array
474
    {
475
        return $this->paddingTopBottomRows;
476
    }
477
478
    public function setPadding(int $topBottom, int $leftRight = null) : self
479
    {
480
        if ($leftRight === null) {
481
            $leftRight = $topBottom;
482
        }
483
484
        $this->setPaddingTopBottom($topBottom);
485
        $this->setPaddingLeftRight($leftRight);
486
487
        $this->calculateContentWidth();
488
        $this->generatePaddingTopBottomRows();
489
490
        return $this;
491
    }
492
493
    public function setPaddingTopBottom(int $topBottom) : self
494
    {
495
        Assertion::greaterOrEqualThan($topBottom, 0);
496
        $this->paddingTopBottom = $topBottom;
497
498
        $this->generatePaddingTopBottomRows();
499
500
        return $this;
501
    }
502
503
    public function setPaddingLeftRight(int $leftRight) : self
504
    {
505
        Assertion::greaterOrEqualThan($leftRight, 0);
506
        $this->paddingLeftRight = $leftRight;
507
508
        $this->calculateContentWidth();
509
        $this->generatePaddingTopBottomRows();
510
511
        return $this;
512
    }
513
514
    public function getMargin() : int
515
    {
516
        return $this->margin;
517
    }
518
519
    public function setMarginAuto() : self
520
    {
521
        $this->marginAuto = true;
522
        $this->margin = 0;
523
524
        $this->setWidth($this->requestedWidth);
525
526
        return $this;
527
    }
528
529
    private function calculateMarginAuto(int $width) : void
530
    {
531
        $this->margin = (int) floor(($this->terminal->getWidth() - ($width)) / 2);
532
    }
533
534
    public function setMargin(int $margin) : self
535
    {
536
        Assertion::greaterOrEqualThan($margin, 0);
537
538
        $this->marginAuto = false;
539
        $this->margin = $margin;
540
541
        //margin + width may now exceed terminal size
542
        //so set width again to trigger width check + maybe resize
543
        $this->setWidth($this->requestedWidth);
544
545
        return $this;
546
    }
547
548
    public function getContentWidth() : int
549
    {
550
        return $this->contentWidth;
551
    }
552
553
    /**
554
     * Get padding for right had side of content
555
     */
556
    public function getRightHandPadding(int $contentLength) : int
557
    {
558
        $rightPadding = $this->getContentWidth() - $contentLength + $this->getPaddingLeftRight();
559
560
        if ($rightPadding < 0) {
561
            $rightPadding = 0;
562
        }
563
564
        return $rightPadding;
565
    }
566
567
    public function setItemExtra(string $itemExtra) : self
568
    {
569
        $this->itemExtra = $itemExtra;
570
571
        return $this;
572
    }
573
574
    public function getItemExtra() : string
575
    {
576
        return $this->itemExtra;
577
    }
578
579
    public function getDisplaysExtra() : bool
580
    {
581
        return $this->displaysExtra;
582
    }
583
584
    public function setDisplaysExtra(bool $displaysExtra) : self
585
    {
586
        $this->displaysExtra = $displaysExtra;
587
588
        return $this;
589
    }
590
591
    public function getTitleSeparator() : string
592
    {
593
        return $this->titleSeparator;
594
    }
595
596
    public function setTitleSeparator(string $actionSeparator) : self
597
    {
598
        $this->titleSeparator = $actionSeparator;
599
600
        return $this;
601
    }
602
603
    private function generateBorderRows() : void
604
    {
605
        $borderRow = sprintf(
606
            "%s%s%s%s\n",
607
            $this->debugMode ? $this->getDebugString($this->margin) : str_repeat(' ', $this->margin),
608
            $this->getBorderColourCode(),
609
            str_repeat(' ', $this->width),
610
            $this->getColoursResetCode()
611
        );
612
613
        if ($this->debugMode && s::length($borderRow) <= $this->terminal->getWidth()) {
614
            $borderRow = substr_replace(
615
                $borderRow,
616
                sprintf("%s\n", $this->getDebugString($this->terminal->getWidth() - (s::length($borderRow) - 1))),
617
                -1
618
            );
619
        }
620
621
        $this->borderTopRows = array_fill(0, $this->borderTopWidth, $borderRow);
622
        $this->borderBottomRows = array_fill(0, $this->borderBottomWidth, $borderRow);
623
    }
624
625
    /**
626
     * @return array
627
     */
628
    public function getBorderTopRows() : array
629
    {
630
        return $this->borderTopRows;
631
    }
632
633
    /**
634
     * @return array
635
     */
636
    public function getBorderBottomRows() : array
637
    {
638
        return $this->borderBottomRows;
639
    }
640
641
    /**
642
     * @param int|string|null $rightWidth
643
     * @param int|string|null $bottomWidth
644
     * @param int|string|null $leftWidth
645
     *
646
     * Shorthand function to set all borders values at once
647
     */
648
    public function setBorder(
649
        int $topWidth,
650
        $rightWidth = null,
651
        $bottomWidth = null,
652
        $leftWidth = null,
653
        string $colour = null
654
    ) : self {
655
        if (!is_int($rightWidth)) {
656
            $colour = $rightWidth;
657
            $rightWidth = $bottomWidth = $leftWidth = $topWidth;
658
        } elseif (!is_int($bottomWidth)) {
659
            $colour = $bottomWidth;
660
            $bottomWidth = $topWidth;
661
            $leftWidth = $rightWidth;
662
        } elseif (!is_int($leftWidth)) {
663
            $colour = $leftWidth;
664
            $leftWidth = $rightWidth;
665
        }
666
667
        $this->borderTopWidth = $topWidth;
668
        $this->borderRightWidth = $rightWidth;
669
        $this->borderBottomWidth = $bottomWidth;
670
        $this->borderLeftWidth = $leftWidth;
671
672
        if (is_string($colour)) {
673
            $this->setBorderColour($colour);
674
        }
675
676
        $this->calculateContentWidth();
677
        $this->generateBorderRows();
678
        $this->generatePaddingTopBottomRows();
679
680
        return $this;
681
    }
682
683
    public function setBorderTopWidth(int $width) : self
684
    {
685
        $this->borderTopWidth = $width;
686
687
        $this->generateBorderRows();
688
689
        return $this;
690
    }
691
692
    public function setBorderRightWidth(int $width) : self
693
    {
694
        $this->borderRightWidth = $width;
695
        $this->calculateContentWidth();
696
697
        $this->generatePaddingTopBottomRows();
698
699
        return $this;
700
    }
701
702
    public function setBorderBottomWidth(int $width) : self
703
    {
704
        $this->borderBottomWidth = $width;
705
706
        $this->generateBorderRows();
707
708
        return $this;
709
    }
710
711
    public function setBorderLeftWidth(int $width) : self
712
    {
713
        $this->borderLeftWidth = $width;
714
        $this->calculateContentWidth();
715
716
        $this->generatePaddingTopBottomRows();
717
718
        return $this;
719
    }
720
721
    public function setBorderColour(string $colour, string $fallback = null) : self
722
    {
723
        $this->borderColour = ColourUtil::validateColour(
724
            $this->terminal,
725
            $colour,
726
            $fallback
727
        );
728
729
        $this->generateBorderRows();
730
        $this->generatePaddingTopBottomRows();
731
732
        return $this;
733
    }
734
735
    public function getBorderTopWidth() : int
736
    {
737
        return $this->borderTopWidth;
738
    }
739
740
    public function getBorderRightWidth() : int
741
    {
742
        return $this->borderRightWidth;
743
    }
744
745
    public function getBorderBottomWidth() : int
746
    {
747
        return $this->borderBottomWidth;
748
    }
749
750
    public function getBorderLeftWidth() : int
751
    {
752
        return $this->borderLeftWidth;
753
    }
754
755
    public function getBorderColour() : string
756
    {
757
        return $this->borderColour;
758
    }
759
760
    public function getBorderColourCode() : string
761
    {
762
        if (!ctype_digit($this->borderColour)) {
763
            $borderColourCode = self::$availableBackgroundColors[$this->borderColour];
764
        } else {
765
            $borderColourCode = sprintf("48;5;%s", $this->borderColour);
766
        }
767
768
        return sprintf("\033[%sm", $borderColourCode);
769
    }
770
771
    /**
772
     * Get a string of given length consisting of 0-9
773
     * eg $length = 15 : 012345678901234
774
     */
775
    private function getDebugString(int $length) : string
776
    {
777
        $nums = [];
778
        for ($i = 0, $j = 0; $i < $length; $i++, $j++) {
779
            if ($j === 10) {
780
                $j = 0;
781
            }
782
783
            $nums[] = $j;
784
        }
785
786
        return implode('', $nums);
787
    }
788
}
789