Completed
Pull Request — master (#222)
by Aydin
03:26 queued 01:22
created

MenuStyle::windowResize()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 16
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

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