Completed
Push — master ( d80c74...3c4db9 )
by Aydin
14s queued 11s
created

MenuStyle::__construct()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 25
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 6
Bugs 0 Features 0
Metric Value
cc 2
eloc 20
c 6
b 0
f 0
nc 1
nop 1
dl 0
loc 25
rs 9.6
1
<?php
2
3
namespace PhpSchool\CliMenu;
4
5
use PhpSchool\CliMenu\Terminal\TerminalFactory;
6
use PhpSchool\CliMenu\Util\ColourUtil;
7
use PhpSchool\Terminal\Terminal;
8
use Assert\Assertion;
9
10
//TODO: B/W fallback
11
12
/**
13
 * @author Michael Woodward <[email protected]>
14
 */
15
class MenuStyle
16
{
17
    /**
18
     * @var Terminal
19
     */
20
    protected $terminal;
21
22
    /**
23
     * @var string
24
     */
25
    protected $fg;
26
27
    /**
28
     * @var string
29
     */
30
    protected $bg;
31
32
    /**
33
     * @var int
34
     */
35
    protected $width;
36
37
    /**
38
     * @var int
39
     */
40
    protected $margin;
41
42
    /**
43
     * @var int
44
     */
45
    protected $paddingTopBottom;
46
47
    /**
48
     * @var int
49
     */
50
    protected $paddingLeftRight;
51
52
    /**
53
     * @var array
54
     */
55
    private $paddingTopBottomRows = [];
56
57
    /**
58
     * @var int
59
     */
60
    protected $contentWidth;
61
62
    /**
63
     * @var string
64
     */
65
    private $selectedMarker;
66
67
    /**
68
     * @var string
69
     */
70
    private $unselectedMarker;
71
72
    /**
73
     * @var string
74
     */
75
    private $checkedMarker;
76
77
    /**
78
     * @var string
79
     */
80
    private $uncheckedMarker;
81
82
    /**
83
     * @var string
84
     */
85
    private $itemExtra;
86
87
    /**
88
     * @var bool
89
     */
90
    private $displaysExtra;
91
92
    /**
93
     * @var string
94
     */
95
    private $titleSeparator;
96
97
    /**
98
     * @var string
99
     */
100
    private $coloursSetCode;
101
102
    /**
103
     * @var string
104
     */
105
    private $invertedColoursSetCode = "\033[7m";
106
107
    /**
108
     * @var string
109
     */
110
    private $invertedColoursUnsetCode = "\033[27m";
111
112
    /**
113
     * @var string
114
     */
115
    private $coloursResetCode = "\033[0m";
116
117
    /**
118
     * @var int
119
     */
120
    private $borderTopWidth;
121
122
    /**
123
     * @var int
124
     */
125
    private $borderRightWidth;
126
127
    /**
128
     * @var int
129
     */
130
    private $borderBottomWidth;
131
132
    /**
133
     * @var int
134
     */
135
    private $borderLeftWidth;
136
137
    /**
138
     * @var string
139
     */
140
    private $borderColour = 'white';
141
142
    /**
143
     * @var array
144
     */
145
    private $borderTopRows = [];
146
147
    /**
148
     * @var array
149
     */
150
    private $borderBottomRows = [];
151
152
    /**
153
     * @var bool
154
     */
155
    private $marginAuto = false;
156
157
    /**
158
     * Default Values
159
     *
160
     * @var array
161
     */
162
    private static $defaultStyleValues = [
163
        'fg' => 'white',
164
        'bg' => 'blue',
165
        'width' => 100,
166
        'paddingTopBottom' => 1,
167
        'paddingLeftRight' => 2,
168
        'margin' => 2,
169
        'selectedMarker' => '● ',
170
        'unselectedMarker' => '○ ',
171
        'checkedMarker' => '[✔] ',
172
        'uncheckedMarker' => '[ ] ',
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 = array(
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 = array(
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 = array(
218
        'bold'       => array('set' => 1, 'unset' => 22),
219
        'dim'        => array('set' => 2, 'unset' => 22),
220
        'underscore' => array('set' => 4, 'unset' => 24),
221
        'blink'      => array('set' => 5, 'unset' => 25),
222
        'reverse'    => array('set' => 7, 'unset' => 27),
223
        'conceal'    => array('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->setSelectedMarker(self::$defaultStyleValues['selectedMarker']);
243
        $this->setUnselectedMarker(self::$defaultStyleValues['unselectedMarker']);
244
        $this->setCheckedMarker(self::$defaultStyleValues['checkedMarker']);
245
        $this->setUncheckedMarker(self::$defaultStyleValues['uncheckedMarker']);
246
        $this->setItemExtra(self::$defaultStyleValues['itemExtra']);
247
        $this->setDisplaysExtra(self::$defaultStyleValues['displaysExtra']);
248
        $this->setTitleSeparator(self::$defaultStyleValues['titleSeparator']);
249
        $this->setBorderTopWidth(self::$defaultStyleValues['borderTopWidth']);
250
        $this->setBorderRightWidth(self::$defaultStyleValues['borderRightWidth']);
251
        $this->setBorderBottomWidth(self::$defaultStyleValues['borderBottomWidth']);
252
        $this->setBorderLeftWidth(self::$defaultStyleValues['borderLeftWidth']);
253
        $this->setBorderColour(self::$defaultStyleValues['borderColour']);
254
    }
255
256
    public function hasChangedFromDefaults() : bool
257
    {
258
        $currentValues = [
259
            $this->fg,
260
            $this->bg,
261
            $this->width,
262
            $this->paddingTopBottom,
263
            $this->paddingLeftRight,
264
            $this->margin,
265
            $this->selectedMarker,
266
            $this->unselectedMarker,
267
            $this->checkedMarker,
268
            $this->uncheckedMarker,
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
        return $currentValues !== array_values(self::$defaultStyleValues);
281
    }
282
283
    public function getDisabledItemText(string $text) : string
284
    {
285
        return sprintf(
286
            "\033[%sm%s\033[%sm",
287
            self::$availableOptions['dim']['set'],
288
            $text,
289
            self::$availableOptions['dim']['unset']
290
        );
291
    }
292
293
    /**
294
     * Generates the ansi escape sequence to set the colours
295
     */
296
    private function generateColoursSetCode() : void
297
    {
298
        if (!ctype_digit($this->fg)) {
299
            $fgCode = self::$availableForegroundColors[$this->fg];
300
        } else {
301
            $fgCode = sprintf("38;5;%s", $this->fg);
302
        }
303
304
        if (!ctype_digit($this->bg)) {
305
            $bgCode = self::$availableBackgroundColors[$this->bg];
306
        } else {
307
            $bgCode = sprintf("48;5;%s", $this->bg);
308
        }
309
310
        $this->coloursSetCode = sprintf("\033[%s;%sm", $fgCode, $bgCode);
311
    }
312
313
    /**
314
     * Get the colour code for Bg and Fg
315
     */
316
    public function getColoursSetCode() : string
317
    {
318
        return $this->coloursSetCode;
319
    }
320
321
    /**
322
     * Get the inverted escape sequence (used for selected elements)
323
     */
324
    public function getInvertedColoursSetCode() : string
325
    {
326
        return $this->invertedColoursSetCode;
327
    }
328
329
    /**
330
     * Get the inverted escape sequence (used for selected elements)
331
     */
332
    public function getInvertedColoursUnsetCode() : string
333
    {
334
        return $this->invertedColoursUnsetCode;
335
    }
336
337
    /**
338
     * Get the escape sequence used to reset colours to default
339
     */
340
    public function getColoursResetCode() : string
341
    {
342
        return $this->coloursResetCode;
343
    }
344
345
    /**
346
     * Calculate the contents width
347
     */
348
    protected function calculateContentWidth() : void
349
    {
350
        $this->contentWidth = $this->width
351
            - ($this->paddingLeftRight * 2)
352
            - ($this->borderRightWidth + $this->borderLeftWidth);
353
354
        if ($this->contentWidth < 0) {
355
            $this->contentWidth = 0;
356
        }
357
    }
358
359
    public function getFg()
360
    {
361
        return $this->fg;
362
    }
363
364
    public function setFg(string $fg, string $fallback = null) : self
365
    {
366
        $this->fg = ColourUtil::validateColour(
367
            $this->terminal,
368
            $fg,
369
            $fallback
370
        );
371
        $this->generateColoursSetCode();
372
373
        return $this;
374
    }
375
376
    public function getBg()
377
    {
378
        return $this->bg;
379
    }
380
381
    public function setBg(string $bg, string $fallback = null) : self
382
    {
383
        $this->bg = ColourUtil::validateColour(
384
            $this->terminal,
385
            $bg,
386
            $fallback
387
        );
388
389
        $this->generateColoursSetCode();
390
        $this->generatePaddingTopBottomRows();
391
392
        return $this;
393
    }
394
395
    public function getWidth() : int
396
    {
397
        return $this->width;
398
    }
399
400
    public function setWidth(int $width) : self
401
    {
402
        Assertion::greaterOrEqualThan($width, 0);
403
404
        if ($width >= $this->terminal->getWidth()) {
405
            $width = $this->terminal->getWidth();
406
        }
407
408
        $this->width = $width;
409
        if ($this->marginAuto) {
410
            $this->setMarginAuto();
411
        }
412
413
        $this->calculateContentWidth();
414
        $this->generateBorderRows();
415
        $this->generatePaddingTopBottomRows();
416
417
        return $this;
418
    }
419
420
    public function getPaddingTopBottom() : int
421
    {
422
        return $this->paddingTopBottom;
423
    }
424
425
    public function getPaddingLeftRight() : int
426
    {
427
        return $this->paddingLeftRight;
428
    }
429
430
    private function generatePaddingTopBottomRows() : void
431
    {
432
        if ($this->borderLeftWidth || $this->borderRightWidth) {
433
            $borderColour = $this->getBorderColourCode();
434
        } else {
435
            $borderColour = '';
436
        }
437
438
        $paddingRow = sprintf(
439
            "%s%s%s%s%s%s%s%s%s%s\n",
440
            str_repeat(' ', $this->margin),
441
            $borderColour,
442
            str_repeat(' ', $this->borderLeftWidth),
443
            $this->getColoursSetCode(),
444
            str_repeat(' ', $this->paddingLeftRight),
445
            str_repeat(' ', $this->contentWidth),
446
            str_repeat(' ', $this->paddingLeftRight),
447
            $borderColour,
448
            str_repeat(' ', $this->borderRightWidth),
449
            $this->coloursResetCode
450
        );
451
452
        $this->paddingTopBottomRows = array_fill(0, $this->paddingTopBottom, $paddingRow);
453
    }
454
455
    public function getPaddingTopBottomRows() : array
456
    {
457
        return $this->paddingTopBottomRows;
458
    }
459
460
    public function setPadding(int $topBottom, int $leftRight = null) : self
461
    {
462
        if ($leftRight === null) {
463
            $leftRight = $topBottom;
464
        }
465
466
        $this->setPaddingTopBottom($topBottom);
467
        $this->setPaddingLeftRight($leftRight);
468
469
        $this->calculateContentWidth();
470
        $this->generatePaddingTopBottomRows();
471
472
        return $this;
473
    }
474
475
    public function setPaddingTopBottom(int $topBottom) : self
476
    {
477
        Assertion::greaterOrEqualThan($topBottom, 0);
478
        $this->paddingTopBottom = $topBottom;
479
480
        $this->generatePaddingTopBottomRows();
481
482
        return $this;
483
    }
484
485
    public function setPaddingLeftRight(int $leftRight) : self
486
    {
487
        Assertion::greaterOrEqualThan($leftRight, 0);
488
        $this->paddingLeftRight = $leftRight;
489
490
        $this->calculateContentWidth();
491
        $this->generatePaddingTopBottomRows();
492
493
        return $this;
494
    }
495
496
    public function getMargin() : int
497
    {
498
        return $this->margin;
499
    }
500
501
    public function setMarginAuto() : self
502
    {
503
        $this->marginAuto = true;
504
        $this->margin = (int) floor(($this->terminal->getWidth() - $this->width) / 2);
505
506
        $this->generateBorderRows();
507
        $this->generatePaddingTopBottomRows();
508
509
        return $this;
510
    }
511
512
    public function setMargin(int $margin) : self
513
    {
514
        Assertion::greaterOrEqualThan($margin, 0);
515
516
        $this->marginAuto = false;
517
        $this->margin = $margin;
518
519
        $this->generateBorderRows();
520
        $this->generatePaddingTopBottomRows();
521
522
        return $this;
523
    }
524
525
    public function getContentWidth() : int
526
    {
527
        return $this->contentWidth;
528
    }
529
530
    /**
531
     * Get padding for right had side of content
532
     */
533
    public function getRightHandPadding(int $contentLength) : int
534
    {
535
        $rightPadding = $this->getContentWidth() - $contentLength + $this->getPaddingLeftRight();
536
537
        if ($rightPadding < 0) {
538
            $rightPadding = 0;
539
        }
540
541
        return $rightPadding;
542
    }
543
544
    public function getSelectedMarker() : string
545
    {
546
        return $this->selectedMarker;
547
    }
548
549
    public function setSelectedMarker(string $marker) : self
550
    {
551
        $this->selectedMarker = $marker;
552
553
        return $this;
554
    }
555
556
    public function getUnselectedMarker() : string
557
    {
558
        return $this->unselectedMarker;
559
    }
560
561
    public function setUnselectedMarker(string $marker) : self
562
    {
563
        $this->unselectedMarker = $marker;
564
565
        return $this;
566
    }
567
568
    /**
569
     * Get the correct marker for the item
570
     */
571
    public function getMarker(bool $selected) : string
572
    {
573
        return $selected ? $this->selectedMarker : $this->unselectedMarker;
574
    }
575
576
    public function getCheckedMarker() : string
577
    {
578
        return $this->checkedMarker;
579
    }
580
581
    public function setCheckedMarker(string $marker) : self
582
    {
583
        $this->checkedMarker = $marker;
584
585
        return $this;
586
    }
587
588
    public function getUncheckedMarker() : string
589
    {
590
        return $this->uncheckedMarker;
591
    }
592
593
    public function setUncheckedMarker(string $marker) : self
594
    {
595
        $this->uncheckedMarker = $marker;
596
597
        return $this;
598
    }
599
600
    public function setItemExtra(string $itemExtra) : self
601
    {
602
        $this->itemExtra = $itemExtra;
603
604
        return $this;
605
    }
606
607
    public function getItemExtra() : string
608
    {
609
        return $this->itemExtra;
610
    }
611
612
    public function getDisplaysExtra() : bool
613
    {
614
        return $this->displaysExtra;
615
    }
616
617
    public function setDisplaysExtra(bool $displaysExtra) : self
618
    {
619
        $this->displaysExtra = $displaysExtra;
620
621
        return $this;
622
    }
623
624
    public function getTitleSeparator() : string
625
    {
626
        return $this->titleSeparator;
627
    }
628
629
    public function setTitleSeparator(string $actionSeparator) : self
630
    {
631
        $this->titleSeparator = $actionSeparator;
632
633
        return $this;
634
    }
635
636
    private function generateBorderRows() : void
637
    {
638
        $borderRow = sprintf(
639
            "%s%s%s%s\n",
640
            str_repeat(' ', $this->margin),
641
            $this->getBorderColourCode(),
642
            str_repeat(' ', $this->width),
643
            $this->coloursResetCode
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