Completed
Push — master ( c7bdb1...5472d7 )
by Aydin
04:34
created

CliMenu::addItem()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 8
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 4
nc 2
nop 1
1
<?php
2
3
namespace PhpSchool\CliMenu;
4
5
use PhpSchool\CliMenu\Exception\InvalidInstantiationException;
6
use PhpSchool\CliMenu\Exception\InvalidTerminalException;
7
use PhpSchool\CliMenu\Exception\MenuNotOpenException;
8
use PhpSchool\CliMenu\MenuItem\LineBreakItem;
9
use PhpSchool\CliMenu\MenuItem\MenuItemInterface;
10
use PhpSchool\CliMenu\MenuItem\StaticItem;
11
use PhpSchool\CliMenu\Dialogue\Confirm;
12
use PhpSchool\CliMenu\Dialogue\Flash;
13
use PhpSchool\CliMenu\Terminal\TerminalFactory;
14
use PhpSchool\CliMenu\Terminal\TerminalInterface;
15
use PhpSchool\CliMenu\Util\StringUtil as s;
16
17
/**
18
 * Class CliMenu
19
 *
20
 * @package PhpSchool\CliMenu
21
 * @author Michael Woodward <[email protected]>
22
 */
23
class CliMenu
24
{
25
    /**
26
     * @var TerminalInterface
27
     */
28
    protected $terminal;
29
30
    /**
31
     * @var MenuStyle
32
     */
33
    protected $style;
34
35
    /**
36
     * @var string
37
     */
38
    protected $title;
39
40
    /**
41
     * @var MenuItemInterface[]
42
     */
43
    protected $items = [];
44
45
    /**
46
     * @var int
47
     */
48
    protected $selectedItem;
49
50
    /**
51
     * @var bool
52
     */
53
    protected $open = false;
54
55
    /**
56
     * @var CliMenu|null
57
     */
58
    protected $parent;
59
60
    /**
61
     * @var Frame|null
62
     */
63
    private $currentFrame;
64
65
    /**
66
     * @param string $title
67
     * @param array $items
68
     * @param TerminalInterface|null $terminal
69
     * @param MenuStyle|null $style
70
     *
71
     * @throws InvalidTerminalException
72
     */
73
    public function __construct(
74
        $title,
75
        array $items,
76
        TerminalInterface $terminal = null,
77
        MenuStyle $style = null
78
    ) {
79
        $this->title      = $title;
80
        $this->items      = $items;
81
        $this->terminal   = $terminal ?: TerminalFactory::fromSystem();
82
        $this->style      = $style ?: new MenuStyle($this->terminal);
83
84
        $this->selectFirstItem();
85
    }
86
87
    /**
88
     * Configure the terminal to work with CliMenu
89
     *
90
     * @throws InvalidTerminalException
91
     */
92
    protected function configureTerminal()
93
    {
94
        $this->assertTerminalIsValidTTY();
95
96
        $this->terminal->setCanonicalMode();
97
        $this->terminal->disableCursor();
98
        $this->terminal->clear();
99
    }
100
101
    /**
102
     * Revert changes made to the terminal
103
     *
104
     * @throws InvalidTerminalException
105
     */
106
    protected function tearDownTerminal()
107
    {
108
        $this->assertTerminalIsValidTTY();
109
110
        $this->terminal->setCanonicalMode(false);
111
        $this->terminal->enableCursor();
112
    }
113
114
    private function assertTerminalIsValidTTY()
115
    {
116
        if (!$this->terminal->isTTY()) {
117
            throw new InvalidTerminalException(
118
                sprintf('Terminal "%s" is not a valid TTY', $this->terminal->getDetails())
119
            );
120
        }
121
    }
122
123
    /**
124
     * @param CliMenu $parent
125
     */
126
    public function setParent(CliMenu $parent)
127
    {
128
        $this->parent = $parent;
129
    }
130
131
    /**
132
     * @return CliMenu|null
133
     */
134
    public function getParent()
135
    {
136
        return $this->parent;
137
    }
138
139
    /**
140
     * @return TerminalInterface
141
     */
142
    public function getTerminal()
143
    {
144
        return $this->terminal;
145
    }
146
147
    /**
148
     * @return bool
149
     */
150
    public function isOpen()
151
    {
152
        return $this->open;
153
    }
154
155
    /**
156
     * Add a new Item to the listing
157
     *
158
     * @param MenuItemInterface $item
159
     */
160
    public function addItem(MenuItemInterface $item)
161
    {
162
        $this->items[] = $item;
163
        
164
        if (count($this->items) === 1) {
165
            $this->selectFirstItem();
166
        }
167
    }
168
169
    /**
170
     * Set the selected pointer to the first selectable item
171
     */
172
    private function selectFirstItem()
173
    {
174
        foreach ($this->items as $key => $item) {
175
            if ($item->canSelect()) {
176
                $this->selectedItem = $key;
177
                break;
178
            }
179
        }
180
    }
181
182
    /**
183
     * Display menu and capture input
184
     */
185
    private function display()
186
    {
187
        $this->draw();
188
189
        while ($this->isOpen() && $input = $this->terminal->getKeyedInput()) {
190
            switch ($input) {
191
                case 'up':
192
                case 'down':
193
                    $this->moveSelection($input);
194
                    $this->draw();
195
                    break;
196
                case 'enter':
197
                    $this->executeCurrentItem();
198
                    break;
199
            }
200
        }
201
    }
202
203
    /**
204
     * Move the selection in a given direction, up / down
205
     *
206
     * @param $direction
207
     */
208
    protected function moveSelection($direction)
209
    {
210
        do {
211
            $itemKeys = array_keys($this->items);
212
213
            $direction === 'up'
214
                ? $this->selectedItem--
215
                : $this->selectedItem++;
216
217
            if (!array_key_exists($this->selectedItem, $this->items)) {
218
                $this->selectedItem  = $direction === 'up'
0 ignored issues
show
Documentation Bug introduced by
It seems like $direction === 'up' ? en...eys) : reset($itemKeys) can also be of type false. However, the property $selectedItem is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
219
                    ? end($itemKeys)
220
                    : reset($itemKeys);
221
            } elseif ($this->getSelectedItem()->canSelect()) {
222
                return;
223
            }
224
        } while (!$this->getSelectedItem()->canSelect());
225
    }
226
227
    /**
228
     * @return MenuItemInterface
229
     */
230
    public function getSelectedItem()
231
    {
232
        return $this->items[$this->selectedItem];
233
    }
234
235
    /**
236
     * Execute the current item
237
     */
238
    protected function executeCurrentItem()
239
    {
240
        $item = $this->getSelectedItem();
241
242
        if ($item->canSelect()) {
243
            $callable = $item->getSelectAction();
244
            $callable($this);
245
        }
246
    }
247
248
    /**
249
     * Redraw the menu
250
     */
251
    public function redraw()
252
    {
253
        if (!$this->isOpen()) {
254
            throw new MenuNotOpenException;
255
        }
256
257
        $this->draw();
258
    }
259
260
    /**
261
     * Draw the menu to STDOUT
262
     */
263
    protected function draw()
264
    {
265
        $this->terminal->clean();
266
        $this->terminal->moveCursorToTop();
267
268
        $frame = new Frame;
269
270
        $frame->newLine(2);
271
272
        if (is_string($this->title)) {
273
            $frame->addRows($this->drawMenuItem(new LineBreakItem()));
274
            $frame->addRows($this->drawMenuItem(new StaticItem($this->title)));
275
            $frame->addRows($this->drawMenuItem(new LineBreakItem($this->style->getTitleSeparator())));
276
        }
277
278
        array_map(function ($item, $index) use ($frame) {
279
            $frame->addRows($this->drawMenuItem($item, $index === $this->selectedItem));
280
        }, $this->items, array_keys($this->items));
281
282
        $frame->addRows($this->drawMenuItem(new LineBreakItem()));
283
284
        $frame->newLine(2);
285
286
        foreach ($frame->getRows() as $row) {
287
            echo $row;
288
        }
289
290
        $this->currentFrame = $frame;
291
    }
292
293
    /**
294
     * Draw a menu item
295
     *
296
     * @param MenuItemInterface $item
297
     * @param bool|false $selected
298
     * @return array
299
     */
300
    protected function drawMenuItem(MenuItemInterface $item, $selected = false)
301
    {
302
        $rows = $item->getRows($this->style, $selected);
303
304
        $setColour = $selected
305
            ? $this->style->getSelectedSetCode()
306
            : $this->style->getUnselectedSetCode();
307
308
        $unsetColour = $selected
309
            ? $this->style->getSelectedUnsetCode()
310
            : $this->style->getUnselectedUnsetCode();
311
312
        return array_map(function ($row) use ($setColour, $unsetColour) {
313
            return sprintf(
314
                "%s%s%s%s%s%s%s\n\r",
315
                str_repeat(' ', $this->style->getMargin()),
316
                $setColour,
317
                str_repeat(' ', $this->style->getPadding()),
318
                $row,
319
                str_repeat(' ', $this->style->getRightHandPadding(mb_strlen(s::stripAnsiEscapeSequence($row)))),
320
                $unsetColour,
321
                str_repeat(' ', $this->style->getMargin())
322
            );
323
        }, $rows);
324
    }
325
326
    /**
327
     * @throws InvalidTerminalException
328
     */
329
    public function open()
330
    {
331
        if ($this->isOpen()) {
332
            return;
333
        }
334
335
        $this->configureTerminal();
336
        $this->open = true;
337
        $this->display();
338
    }
339
340
    /**
341
     * Close the menu
342
     *
343
     * @throws InvalidTerminalException
344
     */
345
    public function close()
346
    {
347
        $menu = $this;
348
349
        do {
350
            $menu->closeThis();
351
            $menu = $menu->getParent();
352
        } while (null !== $menu);
353
        
354
        $this->tearDownTerminal();
355
    }
356
357
    /**
358
     * @throws InvalidTerminalException
359
     */
360
    public function closeThis()
361
    {
362
        $this->terminal->clean();
363
        $this->terminal->moveCursorToTop();
364
        $this->open = false;
365
    }
366
367
    /**
368
     * @return MenuItemInterface[]
369
     */
370
    public function getItems()
371
    {
372
        return $this->items;
373
    }
374
375
    /**
376
     * @param MenuItemInterface $item
377
     */
378
    public function removeItem(MenuItemInterface $item)
379
    {
380
        $key = array_search($item, $this->items);
381
382
        if (false === $key) {
383
            throw new \InvalidArgumentException('Item does not exist in menu');
384
        }
385
386
        unset($this->items[$key]);
387
    }
388
389
    /**
390
     * @return MenuStyle
391
     */
392
    public function getStyle()
393
    {
394
        return $this->style;
395
    }
396
397
    public function getCurrentFrame()
398
    {
399
        return $this->currentFrame;
400
    }
401
402
    /**
403
     * @param string $text
404
     * @return Flash
405
     */
406 View Code Duplication
    public function flash($text)
0 ignored issues
show
Duplication introduced by
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...
407
    {
408
        if (strpos($text, "\n") !== false) {
409
            throw new \InvalidArgumentException;
410
        }
411
412
        $style = (new MenuStyle($this->terminal))
413
            ->setBg('yellow')
414
            ->setFg('red');
415
416
        return new Flash($this, $style, $this->terminal, $text);
417
    }
418
419
    /**
420
     * @param string $text
421
     * @return Confirm
422
     */
423 View Code Duplication
    public function confirm($text)
0 ignored issues
show
Duplication introduced by
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...
424
    {
425
        if (strpos($text, "\n") !== false) {
426
            throw new \InvalidArgumentException;
427
        }
428
429
        $style = (new MenuStyle($this->terminal))
430
            ->setBg('yellow')
431
            ->setFg('red');
432
433
        return new Confirm($this, $style, $this->terminal, $text);
434
    }
435
}
436