Passed
Pull Request — master (#230)
by Aydin
01:57
created

CliMenuBuilder::extractShortcut()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 13
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 6
nc 4
nop 1
dl 0
loc 13
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace PhpSchool\CliMenu\Builder;
4
5
use PhpSchool\CliMenu\Action\ExitAction;
6
use PhpSchool\CliMenu\Action\GoBackAction;
7
use PhpSchool\CliMenu\Exception\InvalidShortcutException;
8
use PhpSchool\CliMenu\MenuItem\AsciiArtItem;
9
use PhpSchool\CliMenu\MenuItem\CheckboxItem;
10
use PhpSchool\CliMenu\MenuItem\LineBreakItem;
11
use PhpSchool\CliMenu\MenuItem\MenuItemInterface;
12
use PhpSchool\CliMenu\MenuItem\MenuMenuItem;
13
use PhpSchool\CliMenu\MenuItem\RadioItem;
14
use PhpSchool\CliMenu\MenuItem\SelectableItem;
15
use PhpSchool\CliMenu\CliMenu;
16
use PhpSchool\CliMenu\MenuItem\SplitItem;
17
use PhpSchool\CliMenu\MenuItem\StaticItem;
18
use PhpSchool\CliMenu\MenuStyle;
19
use PhpSchool\CliMenu\Style\CheckboxStyle;
20
use PhpSchool\CliMenu\Style\Customisable;
0 ignored issues
show
Bug introduced by
The type PhpSchool\CliMenu\Style\Customisable was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
21
use PhpSchool\CliMenu\Style\DefaultStyle;
22
use PhpSchool\CliMenu\Style\RadioStyle;
23
use PhpSchool\CliMenu\Style\SelectableStyle;
24
use PhpSchool\CliMenu\Terminal\TerminalFactory;
25
use PhpSchool\Terminal\Terminal;
26
use function PhpSchool\CliMenu\Util\each;
27
28
/**
29
 * @author Michael Woodward <[email protected]>
30
 * @author Aydin Hassan <[email protected]>
31
 */
32
class CliMenuBuilder
33
{
34
    /**
35
     * @var CliMenu
36
     */
37
    private $menu;
38
39
    /**
40
     * @var string
41
     */
42
    private $goBackButtonText = 'Go Back';
43
44
    /**
45
     * @var string
46
     */
47
    private $exitButtonText = 'Exit';
48
49
    /**
50
     * @var MenuStyle
51
     */
52
    private $style;
53
54
    /**
55
     * @var Terminal
56
     */
57
    private $terminal;
58
59
    /**
60
     * @var bool
61
     */
62
    private $disableDefaultItems = false;
63
64
    /**
65
     * @var bool
66
     */
67
    private $disabled = false;
68
69
    /**
70
     * Whether or not to auto create keyboard shortcuts for items
71
     * when they contain square brackets. Eg: [M]y item
72
     *
73
     * @var bool
74
     */
75
    private $autoShortcuts = false;
76
77
    /**
78
     * Regex to auto match for shortcuts defaults to looking
79
     * for a single character encased in square brackets
80
     *
81
     * @var string
82
     */
83
    private $autoShortcutsRegex = '/\[(.)\]/';
84
85
    /**
86
     * @var bool
87
     */
88
    private $subMenu = false;
89
90
    public function __construct(Terminal $terminal = null)
91
    {
92
        $this->terminal = $terminal ?? TerminalFactory::fromSystem();
93
        $this->style    = new MenuStyle($this->terminal);
94
        $this->menu     = new CliMenu(null, [], $this->terminal, $this->style);
95
    }
96
97
    public static function newSubMenu(Terminal $terminal) : self
98
    {
99
        $instance = new self($terminal);
100
        $instance->subMenu = true;
101
102
        return $instance;
103
    }
104
105
    public function setTitle(string $title) : self
106
    {
107
        $this->menu->setTitle($title);
108
109
        return $this;
110
    }
111
112
    public function addMenuItem(MenuItemInterface $item) : self
113
    {
114
        $this->menu->addItem($item);
115
116
        $this->processItemShortcut($item);
117
118
        return $this;
119
    }
120
121
    public function addItem(
122
        string $text,
123
        callable $itemCallable,
124
        bool $showItemExtra = false,
125
        bool $disabled = false
126
    ) : self {
127
        $this->addMenuItem(new SelectableItem($text, $itemCallable, $showItemExtra, $disabled));
128
129
        return $this;
130
    }
131
132
    public function addItems(array $items) : self
133
    {
134
        foreach ($items as $item) {
135
            $this->addItem(...$item);
0 ignored issues
show
Bug introduced by
The call to PhpSchool\CliMenu\Builde...iMenuBuilder::addItem() has too few arguments starting with itemCallable. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

135
            $this->/** @scrutinizer ignore-call */ 
136
                   addItem(...$item);

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
136
        }
137
138
        return $this;
139
    }
140
141
    public function addCheckboxItem(
142
        string $text,
143
        callable $itemCallable,
144
        bool $showItemExtra = false,
145
        bool $disabled = false
146
    ) : self {
147
        $this->addMenuItem(new CheckboxItem($text, $itemCallable, $showItemExtra, $disabled));
148
149
        return $this;
150
    }
151
152
    public function addRadioItem(
153
        string $text,
154
        callable $itemCallable,
155
        bool $showItemExtra = false,
156
        bool $disabled = false
157
    ) : self {
158
        $this->addMenuItem(new RadioItem($text, $itemCallable, $showItemExtra, $disabled));
159
160
        return $this;
161
    }
162
163
    public function addStaticItem(string $text) : self
164
    {
165
        $this->addMenuItem(new StaticItem($text));
166
167
        return $this;
168
    }
169
170
    public function addLineBreak(string $breakChar = ' ', int $lines = 1) : self
171
    {
172
        $this->addMenuItem(new LineBreakItem($breakChar, $lines));
173
174
        return $this;
175
    }
176
177
    public function addAsciiArt(string $art, string $position = AsciiArtItem::POSITION_CENTER, string $alt = '') : self
178
    {
179
        $this->addMenuItem(new AsciiArtItem($art, $position, $alt));
180
181
        return $this;
182
    }
183
184
    public function addSubMenu(string $text, \Closure $callback) : self
185
    {
186
        $builder = self::newSubMenu($this->terminal);
187
188
        if ($this->autoShortcuts) {
189
            $builder->enableAutoShortcuts($this->autoShortcutsRegex);
190
        }
191
192
        $callback($builder);
193
194
        $menu = $builder->build();
195
        $menu->setParent($this->menu);
196
197
        $this->menu->addItem($item = new MenuMenuItem(
198
            $text,
199
            $menu,
200
            $builder->isMenuDisabled()
201
        ));
202
203
        $this->processItemShortcut($item);
204
205
        return $this;
206
    }
207
208
    public function addSubMenuFromBuilder(string $text, CliMenuBuilder $builder) : self
209
    {
210
        $menu = $builder->build();
211
        $menu->setParent($this->menu);
212
213
        $this->menu->addItem($item = new MenuMenuItem(
214
            $text,
215
            $menu,
216
            $builder->isMenuDisabled()
217
        ));
218
219
        $this->processItemShortcut($item);
220
221
        return $this;
222
    }
223
224
    public function enableAutoShortcuts(string $regex = null) : self
225
    {
226
        $this->autoShortcuts = true;
227
228
        if (null !== $regex) {
229
            $this->autoShortcutsRegex = $regex;
230
        }
231
232
        return $this;
233
    }
234
235
    private function extractShortcut(string $title) : ?string
236
    {
237
        preg_match($this->autoShortcutsRegex, $title, $match);
238
239
        if (!isset($match[1])) {
240
            return null;
241
        }
242
243
        if (mb_strlen($match[1]) > 1) {
244
            throw InvalidShortcutException::fromShortcut($match[1]);
245
        }
246
247
        return isset($match[1]) ? strtolower($match[1]) : null;
248
    }
249
250
    private function processItemShortcut(MenuItemInterface $item) : void
251
    {
252
        $this->processIndividualShortcut($item, function (CliMenu $menu) use ($item) {
253
            $menu->executeAsSelected($item);
254
        });
255
    }
256
257
    private function processSplitItemShortcuts(SplitItem $splitItem) : void
258
    {
259
        foreach ($splitItem->getItems() as $item) {
260
            $this->processIndividualShortcut($item, function (CliMenu $menu) use ($splitItem, $item) {
261
                $current = $splitItem->getSelectedItemIndex();
262
263
                $splitItem->setSelectedItemIndex(
264
                    array_search($item, $splitItem->getItems(), true)
0 ignored issues
show
Bug introduced by
It seems like array_search($item, $splitItem->getItems(), true) can also be of type false and string; however, parameter $index of PhpSchool\CliMenu\MenuIt...:setSelectedItemIndex() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

264
                    /** @scrutinizer ignore-type */ array_search($item, $splitItem->getItems(), true)
Loading history...
265
                );
266
267
                $menu->executeAsSelected($splitItem);
268
269
                if ($current !== null) {
270
                    $splitItem->setSelectedItemIndex($current);
271
                }
272
            });
273
        }
274
    }
275
276
    private function processIndividualShortcut(MenuItemInterface $item, callable $callback) : void
277
    {
278
        if (!$this->autoShortcuts) {
279
            return;
280
        }
281
282
        if ($shortcut = $this->extractShortcut($item->getText())) {
283
            $this->menu->addCustomControlMapping(
284
                $shortcut,
285
                $callback
286
            );
287
        }
288
    }
289
290
    public function addSplitItem(\Closure $callback) : self
291
    {
292
        $builder = new SplitItemBuilder($this->menu);
293
294
        if ($this->autoShortcuts) {
295
            $builder->enableAutoShortcuts($this->autoShortcutsRegex);
296
        }
297
298
        $callback($builder);
299
300
        $this->menu->addItem($splitItem = $builder->build());
301
302
        $this->processSplitItemShortcuts($splitItem);
303
304
        return $this;
305
    }
306
307
    /**
308
     * Disable a submenu
309
     *
310
     * @throws \InvalidArgumentException
311
     */
312
    public function disableMenu() : self
313
    {
314
        if (!$this->subMenu) {
315
            throw new \InvalidArgumentException(
316
                'You can\'t disable the root menu'
317
            );
318
        }
319
320
        $this->disabled = true;
321
322
        return $this;
323
    }
324
325
    public function isMenuDisabled() : bool
326
    {
327
        return $this->disabled;
328
    }
329
330
    public function setGoBackButtonText(string $goBackButtonTest) : self
331
    {
332
        $this->goBackButtonText = $goBackButtonTest;
333
334
        return $this;
335
    }
336
337
    public function setExitButtonText(string $exitButtonText) : self
338
    {
339
        $this->exitButtonText = $exitButtonText;
340
341
        return $this;
342
    }
343
344
    public function setBackgroundColour(string $colour, string $fallback = null) : self
345
    {
346
        $this->style->setBg($colour, $fallback);
347
348
        return $this;
349
    }
350
351
    public function setForegroundColour(string $colour, string $fallback = null) : self
352
    {
353
        $this->style->setFg($colour, $fallback);
354
355
        return $this;
356
    }
357
358
    public function setWidth(int $width) : self
359
    {
360
        $this->style->setWidth($width);
361
362
        return $this;
363
    }
364
365
    public function setPadding(int $topBottom, int $leftRight = null) : self
366
    {
367
        $this->style->setPadding($topBottom, $leftRight);
368
369
        return $this;
370
    }
371
372
    public function setPaddingTopBottom(int $topBottom) : self
373
    {
374
        $this->style->setPaddingTopBottom($topBottom);
375
376
        return $this;
377
    }
378
379
    public function setPaddingLeftRight(int $leftRight) : self
380
    {
381
        $this->style->setPaddingLeftRight($leftRight);
382
383
        return $this;
384
    }
385
386
    public function setMarginAuto() : self
387
    {
388
        $this->style->setMarginAuto();
389
390
        return $this;
391
    }
392
393
    public function setMargin(int $margin) : self
394
    {
395
        $this->style->setMargin($margin);
396
397
        return $this;
398
    }
399
400
    public function setItemExtra(string $extra) : self
401
    {
402
        $this->style->setItemExtra($extra);
403
        $this->getSelectableStyle()->setItemExtra($extra);
404
405
        // if we customise item extra, it means we most likely want to display it
406
        $this->displayExtra();
407
408
        return $this;
409
    }
410
411
    public function setTitleSeparator(string $separator) : self
412
    {
413
        $this->style->setTitleSeparator($separator);
414
415
        return $this;
416
    }
417
418
    /**
419
     * @param int|string|null $right
420
     * @param int|string|null $bottom
421
     * @param int|string|null $left
422
     */
423
    public function setBorder(int $top, $right = null, $bottom = null, $left = null, string $colour = null) : self
424
    {
425
        $this->style->setBorder($top, $right, $bottom, $left, $colour);
426
427
        return $this;
428
    }
429
430
    public function setBorderTopWidth(int $width) : self
431
    {
432
        $this->style->setBorderTopWidth($width);
433
434
        return $this;
435
    }
436
437
    public function setBorderRightWidth(int $width) : self
438
    {
439
        $this->style->setBorderRightWidth($width);
440
441
        return $this;
442
    }
443
444
    public function setBorderBottomWidth(int $width) : self
445
    {
446
        $this->style->setBorderBottomWidth($width);
447
448
        return $this;
449
    }
450
451
    public function setBorderLeftWidth(int $width) : self
452
    {
453
        $this->style->setBorderLeftWidth($width);
454
455
        return $this;
456
    }
457
458
    public function setBorderColour(string $colour, string $fallback = null) : self
459
    {
460
        $this->style->setBorderColour($colour, $fallback);
461
462
        return $this;
463
    }
464
465
    public function getStyle() : MenuStyle
466
    {
467
        return $this->style;
468
    }
469
470
    public function getTerminal() : Terminal
471
    {
472
        return $this->terminal;
473
    }
474
475
    private function getDefaultItems() : array
476
    {
477
        $actions = [];
478
        if ($this->subMenu) {
479
            $actions[] = new SelectableItem($this->goBackButtonText, new GoBackAction);
480
        }
481
482
        $actions[] = new SelectableItem($this->exitButtonText, new ExitAction);
483
        return $actions;
484
    }
485
486
    public function disableDefaultItems() : self
487
    {
488
        $this->disableDefaultItems = true;
489
490
        return $this;
491
    }
492
493
    public function displayExtra() : self
494
    {
495
        $this->style->setDisplaysExtra(true);
496
        $this->getSelectableStyle()->setDisplaysExtra(true);
497
498
        return $this;
499
    }
500
501
    private function itemsHaveExtra(array $items) : bool
502
    {
503
        return !empty(array_filter($items, function (MenuItemInterface $item) {
504
            return $item->showsItemExtra();
505
        }));
506
    }
507
508
    public function build() : CliMenu
509
    {
510
        if (!$this->disableDefaultItems) {
511
            $this->menu->addItems($this->getDefaultItems());
512
        }
513
514
        if (!$this->style->getDisplaysExtra()) {
515
            $this->style->setDisplaysExtra($this->itemsHaveExtra($this->menu->getItems()));
516
        }
517
518
        if (!$this->subMenu) {
519
            $this->menu->propagateStyles();
520
        }
521
522
        return $this->menu;
523
    }
524
525
    public function getDefaultStyle() : DefaultStyle
526
    {
527
        $style = $this->menu->getItemStyle(DefaultStyle::class);
528
        assert($style instanceof DefaultStyle);
529
        return $style;
530
    }
531
532
    public function setDefaultStyle(DefaultStyle $style) : self
533
    {
534
        $this->menu->setItemStyle($style, DefaultStyle::class);
535
536
        return $this;
537
    }
538
539
    public function modifyDefaultStyle(callable $itemCallable) : self
540
    {
541
        $itemCallable($this->getDefaultStyle());
542
543
        return $this;
544
    }
545
546
    public function getSelectableStyle() : SelectableStyle
547
    {
548
        $style = $this->menu->getItemStyle(SelectableStyle::class);
549
        assert($style instanceof SelectableStyle);
550
        return $style;
551
    }
552
553
    public function setSelectableStyle(SelectableStyle $style) : self
554
    {
555
        $this->menu->setItemStyle($style, SelectableStyle::class);
556
557
        return $this;
558
    }
559
560
    public function modifySelectableStyle(callable $itemCallable) : self
561
    {
562
        $itemCallable($this->getSelectableStyle());
563
564
        return $this;
565
    }
566
567
    public function getCheckboxStyle() : CheckboxStyle
568
    {
569
        $style = $this->menu->getItemStyle(CheckboxStyle::class);
570
        assert($style instanceof CheckboxStyle);
571
        return $style;
572
    }
573
574
    public function setCheckboxStyle(CheckboxStyle $style) : self
575
    {
576
        $this->menu->setItemStyle($style, CheckboxStyle::class);
577
578
        return $this;
579
    }
580
581
    public function modifyCheckboxStyle(callable $itemCallable) : self
582
    {
583
        $itemCallable($this->getCheckboxStyle());
584
585
        return $this;
586
    }
587
588
    public function getRadioStyle() : RadioStyle
589
    {
590
        $style = $this->menu->getItemStyle(RadioStyle::class);
591
        assert($style instanceof RadioStyle);
592
        return $style;
593
    }
594
595
    public function setRadioStyle(RadioStyle $style) : self
596
    {
597
        $this->menu->setItemStyle($style, RadioItem::class);
598
599
        return $this;
600
    }
601
602
    public function modifyRadioStyle(callable $itemCallable) : self
603
    {
604
        $itemCallable($this->getRadioStyle());
605
606
        return $this;
607
    }
608
}
609