Completed
Push — master ( df8184...e3b994 )
by Aydin
02:31
created

src/MenuStyle.php (3 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
namespace PhpSchool\CliMenu;
4
5
use PhpSchool\CliMenu\Exception\InvalidInstantiationException;
6
use PhpSchool\CliMenu\Terminal\TerminalFactory;
7
use PhpSchool\CliMenu\Util\ColourUtil;
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
     * @var int
35
     */
36
    protected $width;
37
38
    /**
39
     * @var int
40
     */
41
    protected $margin;
42
43
    /**
44
     * @var int
45
     */
46
    protected $paddingTopBottom;
47
48
    /**
49
     * @var int
50
     */
51
    protected $paddingLeftRight;
52
53
    /**
54
     * @var array
55
     */
56
    private $paddingTopBottomRows = [];
57
58
    /**
59
     * @var int
60
     */
61
    protected $contentWidth;
62
63
    /**
64
     * @var string
65
     */
66
    private $selectedMarker;
67
68
    /**
69
     * @var string
70
     */
71
    private $unselectedMarker;
72
73
    /**
74
     * @var string
75
     */
76
    private $itemExtra;
77
78
    /**
79
     * @var bool
80
     */
81
    private $displaysExtra;
82
83
    /**
84
     * @var string
85
     */
86
    private $titleSeparator;
87
88
    /**
89
     * @var string
90
     */
91
    private $coloursSetCode;
92
93
    /**
94
     * @var string
95
     */
96
    private $invertedColoursSetCode = "\033[7m";
97
98
    /**
99
     * @var string
100
     */
101
    private $invertedColoursUnsetCode = "\033[27m";
102
103
    /**
104
     * @var string
105
     */
106
    private $coloursResetCode = "\033[0m";
107
108
    /**
109
     * @var int
110
     */
111
    private $borderTopWidth;
112
113
    /**
114
     * @var int
115
     */
116
    private $borderRightWidth;
117
118
    /**
119
     * @var int
120
     */
121
    private $borderBottomWidth;
122
123
    /**
124
     * @var int
125
     */
126
    private $borderLeftWidth;
127
128
    /**
129
     * @var string
130
     */
131
    private $borderColour = 'white';
132
133
    /**
134
     * @var array
135
     */
136
    private $borderTopRows = [];
137
138
    /**
139
     * @var array
140
     */
141
    private $borderBottomRows = [];
142
143
    /**
144
     * @var bool
145
     */
146
    private $marginAuto = false;
147
148
    /**
149
     * Default Values
150
     *
151
     * @var array
152
     */
153
    private static $defaultStyleValues = [
154
        'fg' => 'white',
155
        'bg' => 'blue',
156
        'width' => 100,
157
        'paddingTopBottom' => 1,
158
        'paddingLeftRight' => 2,
159
        'margin' => 2,
160
        'selectedMarker' => '●',
161
        'unselectedMarker' => '○',
162
        'itemExtra' => '✔',
163
        'displaysExtra' => false,
164
        'titleSeparator' => '=',
165
        'borderTopWidth' => 0,
166
        'borderRightWidth' => 0,
167
        'borderBottomWidth' => 0,
168
        'borderLeftWidth' => 0,
169
        'borderColour' => 'white',
170
        'borderColourFallback' => null,
171
        'marginAuto' => false,
172
    ];
173
174
    public static function getDefaultStyleValues() : array
175
    {
176
        return static::$defaultStyleValues;
177
    }
178
179
    /**
180
     * @var array
181
     */
182
    private static $availableForegroundColors = array(
183
        'black'   => 30,
184
        'red'     => 31,
185
        'green'   => 32,
186
        'yellow'  => 33,
187
        'blue'    => 34,
188
        'magenta' => 35,
189
        'cyan'    => 36,
190
        'white'   => 37,
191
        'default' => 39,
192
    );
193
194
    /**
195
     * @var array
196
     */
197
    private static $availableBackgroundColors = array(
198
        'black'   => 40,
199
        'red'     => 41,
200
        'green'   => 42,
201
        'yellow'  => 43,
202
        'blue'    => 44,
203
        'magenta' => 45,
204
        'cyan'    => 46,
205
        'white'   => 47,
206
        'default' => 49,
207
    );
208
209
    /**
210
     * @var array
211
     */
212
    private static $availableOptions = array(
213
        'bold'       => array('set' => 1, 'unset' => 22),
214
        'dim'        => array('set' => 2, 'unset' => 22),
215
        'underscore' => array('set' => 4, 'unset' => 24),
216
        'blink'      => array('set' => 5, 'unset' => 25),
217
        'reverse'    => array('set' => 7, 'unset' => 27),
218
        'conceal'    => array('set' => 8, 'unset' => 28)
219
    );
220
221
    /**
222
     * Initialise style
223
     */
224
    public function __construct(Terminal $terminal = null)
225
    {
226
        $this->terminal = $terminal ?: TerminalFactory::fromSystem();
227
228
        $this->fg = static::$defaultStyleValues['fg'];
229
        $this->bg = static::$defaultStyleValues['bg'];
230
231
        $this->generateColoursSetCode();
232
233
        $this->setWidth(static::$defaultStyleValues['width']);
234
        $this->setPaddingTopBottom(static::$defaultStyleValues['paddingTopBottom']);
235
        $this->setPaddingLeftRight(static::$defaultStyleValues['paddingLeftRight']);
236
        $this->setMargin(static::$defaultStyleValues['margin']);
237
        $this->setSelectedMarker(static::$defaultStyleValues['selectedMarker']);
238
        $this->setUnselectedMarker(static::$defaultStyleValues['unselectedMarker']);
239
        $this->setItemExtra(static::$defaultStyleValues['itemExtra']);
240
        $this->setDisplaysExtra(static::$defaultStyleValues['displaysExtra']);
241
        $this->setTitleSeparator(static::$defaultStyleValues['titleSeparator']);
242
        $this->setBorderTopWidth(static::$defaultStyleValues['borderTopWidth']);
243
        $this->setBorderRightWidth(static::$defaultStyleValues['borderRightWidth']);
244
        $this->setBorderBottomWidth(static::$defaultStyleValues['borderBottomWidth']);
245
        $this->setBorderLeftWidth(static::$defaultStyleValues['borderLeftWidth']);
246
        $this->setBorderColour(
247
            static::$defaultStyleValues['borderColour'],
248
            static::$defaultStyleValues['borderColourFallback']
249
        );
250
    }
251
252
    public function getDisabledItemText(string $text) : string
253
    {
254
        return sprintf(
255
            "\033[%sm%s\033[%sm",
256
            self::$availableOptions['dim']['set'],
257
            $text,
258
            self::$availableOptions['dim']['unset']
259
        );
260
    }
261
262
    /**
263
     * Generates the ansi escape sequence to set the colours
264
     */
265
    private function generateColoursSetCode() : void
266
    {
267
        if (!ctype_digit($this->fg)) {
268
            $fgCode = self::$availableForegroundColors[$this->fg];
269
        } else {
270
            $fgCode = sprintf("38;5;%s", $this->fg);
271
        }
272
273
        if (!ctype_digit($this->bg)) {
274
            $bgCode = self::$availableBackgroundColors[$this->bg];
275
        } else {
276
            $bgCode = sprintf("48;5;%s", $this->bg);
277
        }
278
279
        $this->coloursSetCode = sprintf("\033[%s;%sm", $fgCode, $bgCode);
280
    }
281
282
    /**
283
     * Get the colour code for Bg and Fg
284
     */
285
    public function getColoursSetCode() : string
286
    {
287
        return $this->coloursSetCode;
288
    }
289
290
    /**
291
     * Get the inverted escape sequence (used for selected elements)
292
     */
293
    public function getInvertedColoursSetCode() : string
294
    {
295
        return $this->invertedColoursSetCode;
296
    }
297
298
    /**
299
     * Get the inverted escape sequence (used for selected elements)
300
     */
301
    public function getInvertedColoursUnsetCode() : string
302
    {
303
        return $this->invertedColoursUnsetCode;
304
    }
305
306
    /**
307
     * Get the escape sequence used to reset colours to default
308
     */
309
    public function getColoursResetCode() : string
310
    {
311
        return $this->coloursResetCode;
312
    }
313
314
    /**
315
     * Calculate the contents width
316
     */
317
    protected function calculateContentWidth() : void
318
    {
319
        $this->contentWidth = $this->width
320
            - ($this->paddingLeftRight * 2)
321
            - ($this->borderRightWidth + $this->borderLeftWidth);
322
323
        if ($this->contentWidth < 0) {
324
            $this->contentWidth = 0;
325
        }
326
    }
327
328
    public function getFg()
329
    {
330
        return $this->fg;
331
    }
332
333 View Code Duplication
    public function setFg(string $fg, string $fallback = null) : self
0 ignored issues
show
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
334
    {
335
        $this->fg = ColourUtil::validateColour(
336
            $this->terminal,
337
            $fg,
338
            $fallback
339
        );
340
        $this->generateColoursSetCode();
341
342
        return $this;
343
    }
344
345
    public function getBg()
346
    {
347
        return $this->bg;
348
    }
349
350
    public function setBg(string $bg, string $fallback = null) : self
351
    {
352
        $this->bg = ColourUtil::validateColour(
353
            $this->terminal,
354
            $bg,
355
            $fallback
356
        );
357
358
        $this->generateColoursSetCode();
359
        $this->generatePaddingTopBottomRows();
360
361
        return $this;
362
    }
363
364
    public function getWidth() : int
365
    {
366
        return $this->width;
367
    }
368
369
    public function setWidth(int $width) : self
370
    {
371
        Assertion::greaterOrEqualThan($width, 0);
372
373
        if ($width >= $this->terminal->getWidth()) {
374
            $width = $this->terminal->getWidth();
375
        }
376
377
        $this->width = $width;
378
        if ($this->marginAuto) {
379
            $this->setMarginAuto();
380
        }
381
382
        $this->calculateContentWidth();
383
        $this->generateBorderRows();
384
        $this->generatePaddingTopBottomRows();
385
386
        return $this;
387
    }
388
389
    public function getPaddingTopBottom() : int
390
    {
391
        return $this->paddingTopBottom;
392
    }
393
394
    public function getPaddingLeftRight() : int
395
    {
396
        return $this->paddingLeftRight;
397
    }
398
399
    private function generatePaddingTopBottomRows() : void
400
    {
401
        if ($this->borderLeftWidth || $this->borderRightWidth) {
402
            $borderColour = $this->getBorderColourCode();
403
        } else {
404
            $borderColour = '';
405
        }
406
407
        $paddingRow = sprintf(
408
            "%s%s%s%s%s%s%s%s%s%s\n",
409
            str_repeat(' ', $this->margin),
410
            $borderColour,
411
            str_repeat(' ', $this->borderLeftWidth),
412
            $this->getColoursSetCode(),
413
            str_repeat(' ', $this->paddingLeftRight),
414
            str_repeat(' ', $this->contentWidth),
415
            str_repeat(' ', $this->paddingLeftRight),
416
            $borderColour,
417
            str_repeat(' ', $this->borderRightWidth),
418
            $this->coloursResetCode
419
        );
420
421
        $this->paddingTopBottomRows = array_fill(0, $this->paddingTopBottom, $paddingRow);
422
    }
423
424
    public function getPaddingTopBottomRows() : array
425
    {
426
        return $this->paddingTopBottomRows;
427
    }
428
429 View Code Duplication
    public function setPadding(int $topBottom, int $leftRight = null) : self
0 ignored issues
show
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
430
    {
431
        if ($leftRight === null) {
432
            $leftRight = $topBottom;
433
        }
434
435
        $this->setPaddingTopBottom($topBottom);
436
        $this->setPaddingLeftRight($leftRight);
437
438
        $this->calculateContentWidth();
439
        $this->generatePaddingTopBottomRows();
440
441
        return $this;
442
    }
443
444
    public function setPaddingTopBottom(int $topBottom) : self
445
    {
446
        Assertion::greaterOrEqualThan($topBottom, 0);
447
        $this->paddingTopBottom = $topBottom;
448
449
        $this->generatePaddingTopBottomRows();
450
451
        return $this;
452
    }
453
454
    public function setPaddingLeftRight(int $leftRight) : self
455
    {
456
        Assertion::greaterOrEqualThan($leftRight, 0);
457
        $this->paddingLeftRight = $leftRight;
458
459
        $this->calculateContentWidth();
460
        $this->generatePaddingTopBottomRows();
461
462
        return $this;
463
    }
464
465
    public function getMargin() : int
466
    {
467
        return $this->margin;
468
    }
469
470
    public function setMarginAuto() : self
471
    {
472
        $this->marginAuto = true;
473
        $this->margin = floor(($this->terminal->getWidth() - $this->width) / 2);
474
475
        $this->generateBorderRows();
476
        $this->generatePaddingTopBottomRows();
477
478
        return $this;
479
    }
480
481
    public function setMargin(int $margin) : self
482
    {
483
        Assertion::greaterOrEqualThan($margin, 0);
484
485
        $this->marginAuto = false;
486
        $this->margin = $margin;
487
488
        $this->generateBorderRows();
489
        $this->generatePaddingTopBottomRows();
490
491
        return $this;
492
    }
493
494
    public function getContentWidth() : int
495
    {
496
        return $this->contentWidth;
497
    }
498
499
    /**
500
     * Get padding for right had side of content
501
     */
502
    public function getRightHandPadding(int $contentLength) : int
503
    {
504
        $rightPadding = $this->getContentWidth() - $contentLength + $this->getPaddingLeftRight();
505
506
        if ($rightPadding < 0) {
507
            $rightPadding = 0;
508
        }
509
510
        return $rightPadding;
511
    }
512
513
    public function getSelectedMarker() : string
514
    {
515
        return $this->selectedMarker;
516
    }
517
518
    public function setSelectedMarker(string $marker) : self
519
    {
520
        $this->selectedMarker = mb_substr($marker, 0, 1);
521
522
        return $this;
523
    }
524
525
    public function getUnselectedMarker() : string
526
    {
527
        return $this->unselectedMarker;
528
    }
529
530
    public function setUnselectedMarker(string $marker) : self
531
    {
532
        $this->unselectedMarker = mb_substr($marker, 0, 1);
533
534
        return $this;
535
    }
536
537
    /**
538
     * Get the correct marker for the item
539
     */
540
    public function getMarker(bool $selected) : string
541
    {
542
        return $selected ? $this->selectedMarker : $this->unselectedMarker;
543
    }
544
545
    public function setItemExtra(string $itemExtra) : self
546
    {
547
        $this->itemExtra = $itemExtra;
548
549
        return $this;
550
    }
551
552
    public function getItemExtra() : string
553
    {
554
        return $this->itemExtra;
555
    }
556
557
    public function getDisplaysExtra() : bool
558
    {
559
        return $this->displaysExtra;
560
    }
561
562
    public function setDisplaysExtra(bool $displaysExtra) : self
563
    {
564
        $this->displaysExtra = $displaysExtra;
565
566
        return $this;
567
    }
568
569
    public function getTitleSeparator() : string
570
    {
571
        return $this->titleSeparator;
572
    }
573
574
    public function setTitleSeparator(string $actionSeparator) : self
575
    {
576
        $this->titleSeparator = $actionSeparator;
577
578
        return $this;
579
    }
580
581
    private function generateBorderRows() : void
582
    {
583
        $borderRow = sprintf(
584
            "%s%s%s%s\n",
585
            str_repeat(' ', $this->margin),
586
            $this->getBorderColourCode(),
587
            str_repeat(' ', $this->width),
588
            $this->coloursResetCode
589
        );
590
591
        $this->borderTopRows = array_fill(0, $this->borderTopWidth, $borderRow);
592
        $this->borderBottomRows = array_fill(0, $this->borderBottomWidth, $borderRow);
593
    }
594
595
    public function getBorderTopRows() : array
596
    {
597
        return $this->borderTopRows;
598
    }
599
600
    public function getBorderBottomRows() : array
601
    {
602
        return $this->borderBottomRows;
603
    }
604
605
    /**
606
     * Shorthand function to set all borders values at once
607
     */
608
    public function setBorder(
609
        int $topWidth,
610
        $rightWidth = null,
611
        $bottomWidth = null,
612
        $leftWidth = null,
613
        string $colour = null
614
    ) : self {
615 View Code Duplication
        if (!is_int($rightWidth)) {
0 ignored issues
show
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
616
            $colour = $rightWidth;
617
            $rightWidth = $bottomWidth = $leftWidth = $topWidth;
618
        } elseif (!is_int($bottomWidth)) {
619
            $colour = $bottomWidth;
620
            $bottomWidth = $topWidth;
621
            $leftWidth = $rightWidth;
622
        } elseif (!is_int($leftWidth)) {
623
            $colour = $leftWidth;
624
            $leftWidth = $rightWidth;
625
        }
626
627
        $this->borderTopWidth = $topWidth;
628
        $this->borderRightWidth = $rightWidth;
629
        $this->borderBottomWidth = $bottomWidth;
630
        $this->borderLeftWidth = $leftWidth;
631
632
        if (is_string($colour)) {
633
            $this->setBorderColour($colour);
634
        } elseif ($colour !== null) {
635
            throw new \InvalidArgumentException('Invalid colour');
636
        }
637
638
        $this->calculateContentWidth();
639
        $this->generateBorderRows();
640
        $this->generatePaddingTopBottomRows();
641
642
        return $this;
643
    }
644
645
    public function setBorderTopWidth(int $width) : self
646
    {
647
        $this->borderTopWidth = $width;
648
649
        $this->generateBorderRows();
650
651
        return $this;
652
    }
653
654
    public function setBorderRightWidth(int $width) : self
655
    {
656
        $this->borderRightWidth = $width;
657
        $this->calculateContentWidth();
658
659
        $this->generatePaddingTopBottomRows();
660
661
        return $this;
662
    }
663
664
    public function setBorderBottomWidth(int $width) : self
665
    {
666
        $this->borderBottomWidth = $width;
667
668
        $this->generateBorderRows();
669
670
        return $this;
671
    }
672
673
    public function setBorderLeftWidth(int $width) : self
674
    {
675
        $this->borderLeftWidth = $width;
676
        $this->calculateContentWidth();
677
678
        $this->generatePaddingTopBottomRows();
679
680
        return $this;
681
    }
682
683
    public function setBorderColour(string $colour, $fallback = null) : self
684
    {
685
        $this->borderColour = ColourUtil::validateColour(
686
            $this->terminal,
687
            $colour,
688
            $fallback
689
        );
690
691
        $this->generateBorderRows();
692
        $this->generatePaddingTopBottomRows();
693
694
        return $this;
695
    }
696
697
    public function getBorderTopWidth() : int
698
    {
699
        return $this->borderTopWidth;
700
    }
701
702
    public function getBorderRightWidth() : int
703
    {
704
        return $this->borderRightWidth;
705
    }
706
707
    public function getBorderBottomWidth() : int
708
    {
709
        return $this->borderBottomWidth;
710
    }
711
712
    public function getBorderLeftWidth() : int
713
    {
714
        return $this->borderLeftWidth;
715
    }
716
717
    public function getBorderColour() : string
718
    {
719
        return $this->borderColour;
720
    }
721
722
    public function getBorderColourCode() : string
723
    {
724
        if (!ctype_digit($this->borderColour)) {
725
            $borderColourCode = self::$availableBackgroundColors[$this->borderColour];
726
        } else {
727
            $borderColourCode = sprintf("48;5;%s", $this->borderColour);
728
        }
729
730
        return sprintf("\033[%sm", $borderColourCode);
731
    }
732
}
733