Completed
Pull Request — master (#127)
by Aydin
01:46
created

CliMenu::drawMenuItem()   B

Complexity

Conditions 6
Paths 16

Size

Total Lines 39
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 39
rs 8.439
c 0
b 0
f 0
cc 6
eloc 30
nc 16
nop 2
1
<?php
2
3
namespace PhpSchool\CliMenu;
4
5
use PhpSchool\CliMenu\Dialogue\NumberInput;
6
use PhpSchool\CliMenu\Exception\InvalidInstantiationException;
7
use PhpSchool\CliMenu\Exception\InvalidTerminalException;
8
use PhpSchool\CliMenu\Exception\MenuNotOpenException;
9
use PhpSchool\CliMenu\Input\InputIO;
10
use PhpSchool\CliMenu\Input\Number;
11
use PhpSchool\CliMenu\Input\Password;
12
use PhpSchool\CliMenu\Input\Text;
13
use PhpSchool\CliMenu\MenuItem\LineBreakItem;
14
use PhpSchool\CliMenu\MenuItem\MenuItemInterface;
15
use PhpSchool\CliMenu\MenuItem\SplitItem;
16
use PhpSchool\CliMenu\MenuItem\StaticItem;
17
use PhpSchool\CliMenu\Dialogue\Confirm;
18
use PhpSchool\CliMenu\Dialogue\Flash;
19
use PhpSchool\CliMenu\Terminal\TerminalFactory;
20
use PhpSchool\CliMenu\Util\StringUtil as s;
21
use PhpSchool\Terminal\Exception\NotInteractiveTerminal;
22
use PhpSchool\Terminal\InputCharacter;
23
use PhpSchool\Terminal\NonCanonicalReader;
24
use PhpSchool\Terminal\Terminal;
25
use PhpSchool\Terminal\TerminalReader;
26
27
/**
28
 * @author Michael Woodward <[email protected]>
29
 */
30
class CliMenu
31
{
32
    /**
33
     * @var Terminal
34
     */
35
    protected $terminal;
36
37
    /**
38
     * @var MenuStyle
39
     */
40
    protected $style;
41
42
    /**
43
     * @var ?string
44
     */
45
    protected $title;
46
47
    /**
48
     * @var MenuItemInterface[]
49
     */
50
    protected $items = [];
51
52
    /**
53
     * @var int
54
     */
55
    protected $selectedItem;
56
57
    /**
58
     * @var bool
59
     */
60
    protected $open = false;
61
62
    /**
63
     * @var CliMenu|null
64
     */
65
    protected $parent;
66
67
    /**
68
     * @var array
69
     */
70
    protected $defaultControlMappings = [
71
        '^P' => InputCharacter::UP,
72
        'k'  => InputCharacter::UP,
73
        '^K' => InputCharacter::DOWN,
74
        'j'  => InputCharacter::DOWN,
75
        "\r" => InputCharacter::ENTER,
76
        ' '  => InputCharacter::ENTER,
77
        'l'  => InputCharacter::LEFT,
78
        'm'  => InputCharacter::RIGHT,
79
    ];
80
81
    /**
82
     * @var array
83
     */
84
    protected $customControlMappings = [];
85
86
    /**
87
     * @var Frame
88
     */
89
    private $currentFrame;
90
91
    public function __construct(
92
        ?string $title,
93
        array $items,
94
        Terminal $terminal = null,
95
        MenuStyle $style = null
96
    ) {
97
        $this->title      = $title;
98
        $this->items      = $items;
99
        $this->terminal   = $terminal ?: TerminalFactory::fromSystem();
100
        $this->style      = $style ?: new MenuStyle($this->terminal);
101
102
        $this->selectFirstItem();
103
    }
104
105
    /**
106
     * Configure the terminal to work with CliMenu
107
     */
108
    protected function configureTerminal() : void
109
    {
110
        $this->assertTerminalIsValidTTY();
111
112
        $this->terminal->disableCanonicalMode();
113
        $this->terminal->disableEchoBack();
114
        $this->terminal->disableCursor();
115
        $this->terminal->clear();
116
    }
117
118
    /**
119
     * Revert changes made to the terminal
120
     */
121
    protected function tearDownTerminal() : void
122
    {
123
        $this->terminal->restoreOriginalConfiguration();
124
        $this->terminal->enableCursor();
125
    }
126
127
    private function assertTerminalIsValidTTY() : void
128
    {
129
        if (!$this->terminal->isInteractive()) {
130
            throw new InvalidTerminalException('Terminal is not interactive (TTY)');
131
        }
132
    }
133
134
135
    public function setParent(CliMenu $parent) : void
136
    {
137
        $this->parent = $parent;
138
    }
139
140
    public function getParent() : ?CliMenu
141
    {
142
        return $this->parent;
143
    }
144
145
    public function getTerminal() : Terminal
146
    {
147
        return $this->terminal;
148
    }
149
150
    public function isOpen() : bool
151
    {
152
        return $this->open;
153
    }
154
155
    /**
156
     * Add a new Item to the menu
157
     */
158
    public function addItem(MenuItemInterface $item) : void
159
    {
160
        $this->items[] = $item;
161
162
        if (count($this->items) === 1) {
163
            $this->selectFirstItem();
164
        }
165
    }
166
167
    /**
168
     * Add multiple Items to the menu
169
     */
170
    public function addItems(array $items) : void
171
    {
172
        foreach ($items as $item) {
173
            $this->items[] = $item;
174
        }
175
176
        if (count($this->items) === count($items)) {
177
            $this->selectFirstItem();
178
        }
179
    }
180
181
    /**
182
     * Set Items of the menu
183
     */
184
    public function setItems(array $items) : void
185
    {
186
        $this->items = $items;
187
188
        $this->selectFirstItem();
189
    }
190
191
    /**
192
     * Set the selected pointer to the first selectable item
193
     */
194
    private function selectFirstItem() : void
195
    {
196
        foreach ($this->items as $key => $item) {
197
            if ($item->canSelect()) {
198
                $this->selectedItem = $key;
199
                break;
200
            }
201
        }
202
    }
203
204
    /**
205
     * Adds a custom control mapping
206
     */
207
    public function addCustomControlMapping(string $input, callable $callable) : void
208
    {
209
        if (isset($this->defaultControlMappings[$input]) || isset($this->customControlMappings[$input])) {
210
            throw new \InvalidArgumentException('Cannot rebind this input');
211
        }
212
213
        $this->customControlMappings[$input] = $callable;
214
    }
215
216
    /**
217
     * Shorthand function to add multiple custom control mapping at once
218
     */
219
    public function addCustomControlMappings(array $map) : void
220
    {
221
        foreach ($map as $input => $callable) {
222
            $this->addCustomControlMapping($input, $callable);
223
        }
224
    }
225
226
    /**
227
     * Removes a custom control mapping
228
     */
229
    public function removeCustomControlMapping(string $input) : void
230
    {
231
        if (!isset($this->customControlMappings[$input])) {
232
            throw new \InvalidArgumentException('This input is not registered');
233
        }
234
235
        unset($this->customControlMappings[$input]);
236
    }
237
238
    /**
239
     * Display menu and capture input
240
     */
241
    private function display() : void
242
    {
243
        $this->draw();
244
245
        $reader = new NonCanonicalReader($this->terminal);
246
        $reader->addControlMappings($this->defaultControlMappings);
247
248
        while ($this->isOpen() && $char = $reader->readCharacter()) {
249
            if (!$char->isHandledControl()) {
250
                $rawChar = $char->get();
251
                if (isset($this->customControlMappings[$rawChar])) {
252
                    $this->customControlMappings[$rawChar]($this);
253
                }
254
                continue;
255
            }
256
257
            switch ($char->getControl()) {
258
                case InputCharacter::UP:
259
                case InputCharacter::DOWN:
260
                    $this->moveSelectionVertically($char->getControl());
261
                    $this->draw();
262
                    break;
263
                case InputCharacter::LEFT:
264
                case InputCharacter::RIGHT:
265
                    $this->moveSelectionHorizontally($char->getControl());
266
                    $this->draw();
267
                    break;
268
                case InputCharacter::ENTER:
269
                    $this->executeCurrentItem();
270
                    break;
271
            }
272
        }
273
    }
274
275
    /**
276
     * Move the selection in a given direction, up / down
277
     */
278
    protected function moveSelectionVertically(string $direction) : void
279
    {
280
        $itemKeys = array_keys($this->items);
281
282
        do {
283
            $direction === 'UP'
284
                ? $this->selectedItem--
285
                : $this->selectedItem++;
286
287
            if (!array_key_exists($this->selectedItem, $this->items)) {
288
                $this->selectedItem  = $direction === 'UP'
289
                    ? end($itemKeys)
290
                    : reset($itemKeys);
291
            }
292
        } while (!$this->getSelectedItem()->canSelect());
293
    }
294
295
    /**
296
     * Move the selection in a given direction, left / right
297
     */
298
    protected function moveSelectionHorizontally(string $direction) : void
299
    {
300
        if (!$this->items[$this->selectedItem] instanceof SplitItem) {
301
            return;
302
        }
303
304
        $item = $this->items[$this->selectedItem];
305
        $itemKeys = array_keys($item->getItems());
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface PhpSchool\CliMenu\MenuItem\MenuItemInterface as the method getItems() does only exist in the following implementations of said interface: PhpSchool\CliMenu\MenuItem\SplitItem.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
306
        $selectedItemIndex = $item->getSelectedItemIndex();
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface PhpSchool\CliMenu\MenuItem\MenuItemInterface as the method getSelectedItemIndex() does only exist in the following implementations of said interface: PhpSchool\CliMenu\MenuItem\SplitItem.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
307
308
        do {
309
            $direction === 'LEFT'
310
                ? $selectedItemIndex--
311
                : $selectedItemIndex++;
312
            $item->setSelectedItemIndex($selectedItemIndex);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface PhpSchool\CliMenu\MenuItem\MenuItemInterface as the method setSelectedItemIndex() does only exist in the following implementations of said interface: PhpSchool\CliMenu\MenuItem\SplitItem.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
313
314
            if (!array_key_exists($selectedItemIndex, $item->getItems())) {
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface PhpSchool\CliMenu\MenuItem\MenuItemInterface as the method getItems() does only exist in the following implementations of said interface: PhpSchool\CliMenu\MenuItem\SplitItem.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
315
                $selectedItemIndex = $direction === 'LEFT'
316
                    ? end($itemKeys)
317
                    : reset($itemKeys);
318
                $item->setSelectedItemIndex($selectedItemIndex);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface PhpSchool\CliMenu\MenuItem\MenuItemInterface as the method setSelectedItemIndex() does only exist in the following implementations of said interface: PhpSchool\CliMenu\MenuItem\SplitItem.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
319
            }
320
        } while (!$item->getSelectedItem()->canSelect());
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface PhpSchool\CliMenu\MenuItem\MenuItemInterface as the method getSelectedItem() does only exist in the following implementations of said interface: PhpSchool\CliMenu\MenuItem\SplitItem.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
321
    }
322
323
    public function getSelectedItem() : MenuItemInterface
324
    {
325
        $item = $this->items[$this->selectedItem];
326
        return $item instanceof SplitItem
327
            ? $item->getSelectedItem()
328
            : $item;
329
    }
330
331
    /**
332
     * Execute the current item
333
     */
334
    protected function executeCurrentItem() : void
335
    {
336
        $item = $this->getSelectedItem();
337
338
        if ($item->canSelect()) {
339
            $callable = $item->getSelectAction();
340
            $callable($this);
341
        }
342
    }
343
344
    /**
345
     * If true we clear the whole terminal screen, useful
346
     * for example when reducing the width of the menu, to not
347
     * leave leftovers of the previous wider menu.
348
     *
349
     * Redraw the menu
350
     */
351
    public function redraw(bool $clear = false) : void
352
    {
353
        if ($clear) {
354
            $this->terminal->clear();
355
        }
356
357
        $this->assertOpen();
358
        $this->draw();
359
    }
360
361
    private function assertOpen() : void
362
    {
363
        if (!$this->isOpen()) {
364
            throw new MenuNotOpenException;
365
        }
366
    }
367
368
    /**
369
     * Draw the menu to STDOUT
370
     */
371
    protected function draw() : void
372
    {
373
        $frame = new Frame;
374
375
        $frame->newLine(2);
376
377
        if ($this->style->getBorderTopWidth() > 0) {
378
            $frame->addRows($this->style->getBorderTopRows());
379
        }
380
381
        if ($this->style->getPaddingTopBottom() > 0) {
382
            $frame->addRows($this->style->getPaddingTopBottomRows());
383
        }
384
385
        if ($this->title) {
386
            $frame->addRows($this->drawMenuItem(new StaticItem($this->title)));
387
            $frame->addRows($this->drawMenuItem(new LineBreakItem($this->style->getTitleSeparator())));
388
        }
389
390
        array_map(function ($item, $index) use ($frame) {
391
            $frame->addRows($this->drawMenuItem($item, $index === $this->selectedItem));
392
        }, $this->items, array_keys($this->items));
393
394
395
        if ($this->style->getPaddingTopBottom() > 0) {
396
            $frame->addRows($this->style->getPaddingTopBottomRows());
397
        }
398
399
        if ($this->style->getBorderBottomWidth() > 0) {
400
            $frame->addRows($this->style->getBorderBottomRows());
401
        }
402
403
        $frame->newLine(2);
404
405
        $this->terminal->moveCursorToTop();
406
        foreach ($frame->getRows() as $row) {
407
            if ($row == "\n") {
408
                $this->terminal->clearLine();
409
            }
410
            $this->terminal->write($row);
411
        }
412
        $this->terminal->clearDown();
413
414
        $this->currentFrame = $frame;
415
    }
416
417
    /**
418
     * Draw a menu item
419
     */
420
    protected function drawMenuItem(MenuItemInterface $item, bool $selected = false) : array
421
    {
422
        $rows = $item->getRows($this->style, $selected);
423
        
424
        if ($item instanceof SplitItem) {
425
            $selected = false;
426
        }
427
428
        $invertedColoursSetCode = $selected
429
            ? $this->style->getInvertedColoursSetCode()
430
            : '';
431
        $invertedColoursUnsetCode = $selected
432
            ? $this->style->getInvertedColoursUnsetCode()
433
            : '';
434
435
        if ($this->style->getBorderLeftWidth() || $this->style->getBorderRightWidth()) {
436
            $borderColour = $this->style->getBorderColourCode();
437
        } else {
438
            $borderColour = '';
439
        }
440
441
        return array_map(function ($row) use ($invertedColoursSetCode, $invertedColoursUnsetCode, $borderColour) {
442
            return sprintf(
443
                "%s%s%s%s%s%s%s%s%s%s%s%s\n",
444
                str_repeat(' ', $this->style->getMargin()),
445
                $borderColour,
446
                str_repeat(' ', $this->style->getBorderLeftWidth()),
447
                $this->style->getColoursSetCode(),
448
                $invertedColoursSetCode,
449
                str_repeat(' ', $this->style->getPaddingLeftRight()),
450
                $row,
451
                str_repeat(' ', $this->style->getRightHandPadding(mb_strlen(s::stripAnsiEscapeSequence($row)))),
452
                $invertedColoursUnsetCode,
453
                $borderColour,
454
                str_repeat(' ', $this->style->getBorderRightWidth()),
455
                $this->style->getColoursResetCode()
456
            );
457
        }, $rows);
458
    }
459
460
    /**
461
     * @throws InvalidTerminalException
462
     */
463
    public function open() : void
464
    {
465
        if ($this->isOpen()) {
466
            return;
467
        }
468
469
        $this->configureTerminal();
470
        $this->open = true;
471
        $this->display();
472
    }
473
474
    /**
475
     * Close the menu
476
     *
477
     * @throws InvalidTerminalException
478
     */
479
    public function close() : void
480
    {
481
        $menu = $this;
482
483
        do {
484
            $menu->closeThis();
485
            $menu = $menu->getParent();
486
        } while (null !== $menu);
487
488
        $this->tearDownTerminal();
489
    }
490
491
    public function closeThis() : void
492
    {
493
        $this->terminal->clean();
494
        $this->terminal->moveCursorToTop();
495
        $this->open = false;
496
    }
497
498
    /**
499
     * @return MenuItemInterface[]
500
     */
501
    public function getItems() : array
502
    {
503
        return $this->items;
504
    }
505
506
    public function removeItem(MenuItemInterface $item) : void
507
    {
508
        $key = array_search($item, $this->items, true);
509
510
        if (false === $key) {
511
            throw new \InvalidArgumentException('Item does not exist in menu');
512
        }
513
514
        unset($this->items[$key]);
515
        $this->items = array_values($this->items);
516
    }
517
518
    public function getStyle() : MenuStyle
519
    {
520
        return $this->style;
521
    }
522
523
    public function getCurrentFrame() : Frame
524
    {
525
        return $this->currentFrame;
526
    }
527
528
    public function flash(string $text, MenuStyle $style = null) : Flash
529
    {
530
        $this->guardSingleLine($text);
531
532
        $style = $style ?? (new MenuStyle($this->terminal))
533
            ->setBg('yellow')
534
            ->setFg('red');
535
536
        return new Flash($this, $style, $this->terminal, $text);
537
    }
538
539
    public function confirm(string $text, MenuStyle $style = null) : Confirm
540
    {
541
        $this->guardSingleLine($text);
542
543
        $style = $style ?? (new MenuStyle($this->terminal))
544
            ->setBg('yellow')
545
            ->setFg('red');
546
547
        return new Confirm($this, $style, $this->terminal, $text);
548
    }
549
550
    public function askNumber(MenuStyle $style = null) : Number
551
    {
552
        $this->assertOpen();
553
554
        $style = $style ?? (new MenuStyle($this->terminal))
555
            ->setBg('yellow')
556
            ->setFg('red');
557
558
        return new Number(new InputIO($this, $this->terminal), $style);
559
    }
560
561
    public function askText(MenuStyle $style = null) : Text
562
    {
563
        $this->assertOpen();
564
565
        $style = $style ?? (new MenuStyle($this->terminal))
566
            ->setBg('yellow')
567
            ->setFg('red');
568
569
        return new Text(new InputIO($this, $this->terminal), $style);
570
    }
571
572
    public function askPassword(MenuStyle $style = null) : Password
573
    {
574
        $this->assertOpen();
575
576
        $style = $style ?? (new MenuStyle($this->terminal))
577
            ->setBg('yellow')
578
            ->setFg('red');
579
580
        return new Password(new InputIO($this, $this->terminal), $style);
581
    }
582
583
    private function guardSingleLine($text) : void
584
    {
585
        if (strpos($text, "\n") !== false) {
586
            throw new \InvalidArgumentException;
587
        }
588
    }
589
}
590