Completed
Pull Request — master (#194)
by
unknown
02:23
created

MenuStyle::getTitleSeparator()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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