Completed
Pull Request — master (#65)
by
unknown
03:54 queued 11s
created

CliMenu::tearDownTerminal()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 7
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 4
nc 1
nop 0
1
<?php
2
3
namespace PhpSchool\CliMenu;
4
5
use PhpSchool\CliMenu\Dialogue\YesNo;
6
use PhpSchool\CliMenu\Exception\InvalidInstantiationException;
7
use PhpSchool\CliMenu\Exception\InvalidTerminalException;
8
use PhpSchool\CliMenu\Exception\MenuNotOpenException;
9
use PhpSchool\CliMenu\MenuItem\LineBreakItem;
10
use PhpSchool\CliMenu\MenuItem\MenuItemInterface;
11
use PhpSchool\CliMenu\MenuItem\StaticItem;
12
use PhpSchool\CliMenu\Dialogue\Confirm;
13
use PhpSchool\CliMenu\Dialogue\Flash;
14
use PhpSchool\CliMenu\Terminal\TerminalFactory;
15
use PhpSchool\CliMenu\Terminal\TerminalInterface;
16
use PhpSchool\CliMenu\Util\StringUtil as s;
17
18
/**
19
 * Class CliMenu
20
 *
21
 * @package PhpSchool\CliMenu
22
 * @author Michael Woodward <[email protected]>
23
 */
24
class CliMenu
25
{
26
    /**
27
     * @var TerminalInterface
28
     */
29
    protected $terminal;
30
31
    /**
32
     * @var MenuStyle
33
     */
34
    protected $style;
35
36
    /**
37
     * @var string
38
     */
39
    protected $title;
40
41
    /**
42
     * @var MenuItemInterface[]
43
     */
44
    protected $items = [];
45
46
    /**
47
     * @var int
48
     */
49
    protected $selectedItem;
50
51
    /**
52
     * @var bool
53
     */
54
    protected $open = false;
55
56
    /**
57
     * @var CliMenu|null
58
     */
59
    protected $parent;
60
61
    /**
62
     * @var Frame|null
63
     */
64
    private $currentFrame;
65
66
    /**
67
     * @param string $title
68
     * @param array $items
69
     * @param TerminalInterface|null $terminal
70
     * @param MenuStyle|null $style
71
     *
72
     * @throws InvalidTerminalException
73
     */
74
    public function __construct(
75
        $title,
76
        array $items,
77
        TerminalInterface $terminal = null,
78
        MenuStyle $style = null
79
    ) {
80
        $this->title      = $title;
81
        $this->items      = $items;
82
        $this->terminal   = $terminal ?: TerminalFactory::fromSystem();
83
        $this->style      = $style ?: new MenuStyle($this->terminal);
84
85
        $this->selectFirstItem();
86
    }
87
88
    /**
89
     * Configure the terminal to work with CliMenu
90
     *
91
     * @throws InvalidTerminalException
92
     */
93
    protected function configureTerminal()
94
    {
95
        $this->assertTerminalIsValidTTY();
96
97
        $this->terminal->setCanonicalMode();
98
        $this->terminal->disableCursor();
99
        $this->terminal->clear();
100
    }
101
102
    /**
103
     * Revert changes made to the terminal
104
     *
105
     * @throws InvalidTerminalException
106
     */
107
    protected function tearDownTerminal()
108
    {
109
        $this->assertTerminalIsValidTTY();
110
111
        $this->terminal->setCanonicalMode(false);
112
        $this->terminal->enableCursor();
113
    }
114
115
    private function assertTerminalIsValidTTY()
116
    {
117
        if (!$this->terminal->isTTY()) {
118
            throw new InvalidTerminalException(
119
                sprintf('Terminal "%s" is not a valid TTY', $this->terminal->getDetails())
120
            );
121
        }
122
    }
123
124
    /**
125
     * @param CliMenu $parent
126
     */
127
    public function setParent(CliMenu $parent)
128
    {
129
        $this->parent = $parent;
130
    }
131
132
    /**
133
     * @return CliMenu|null
134
     */
135
    public function getParent()
136
    {
137
        return $this->parent;
138
    }
139
140
    /**
141
     * @return TerminalInterface
142
     */
143
    public function getTerminal()
144
    {
145
        return $this->terminal;
146
    }
147
148
    /**
149
     * @return bool
150
     */
151
    public function isOpen()
152
    {
153
        return $this->open;
154
    }
155
156
    /**
157
     * Add a new Item to the listing
158
     *
159
     * @param MenuItemInterface $item
160
     */
161
    public function addItem(MenuItemInterface $item)
162
    {
163
        $this->items[] = $item;
164
        
165
        if (count($this->items) === 1) {
166
            $this->selectFirstItem();
167
        }
168
    }
169
170
    /**
171
     * Set the selected pointer to the first selectable item
172
     */
173
    private function selectFirstItem()
174
    {
175
        foreach ($this->items as $key => $item) {
176
            if ($item->canSelect()) {
177
                $this->selectedItem = $key;
178
                break;
179
            }
180
        }
181
    }
182
183
    /**
184
     * Display menu and capture input
185
     */
186
    private function display()
187
    {
188
        $this->draw();
189
190
        while ($this->isOpen() && $input = $this->terminal->getKeyedInput()) {
191
            switch ($input) {
192
                case 'up':
193
                case 'down':
194
                    $this->moveSelection($input);
195
                    $this->draw();
196
                    break;
197
                case 'enter':
198
                    $this->executeCurrentItem();
199
                    break;
200
            }
201
        }
202
    }
203
204
    /**
205
     * Move the selection in a given direction, up / down
206
     *
207
     * @param $direction
208
     */
209
    protected function moveSelection($direction)
210
    {
211
        do {
212
            $itemKeys = array_keys($this->items);
213
214
            $direction === 'up'
215
                ? $this->selectedItem--
216
                : $this->selectedItem++;
217
218
            if (!array_key_exists($this->selectedItem, $this->items)) {
219
                $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...
220
                    ? end($itemKeys)
221
                    : reset($itemKeys);
222
            } elseif ($this->getSelectedItem()->canSelect()) {
223
                return;
224
            }
225
        } while (!$this->getSelectedItem()->canSelect());
226
    }
227
228
    /**
229
     * @return MenuItemInterface
230
     */
231
    public function getSelectedItem()
232
    {
233
        return $this->items[$this->selectedItem];
234
    }
235
236
    /**
237
     * Execute the current item
238
     */
239
    protected function executeCurrentItem()
240
    {
241
        $item = $this->getSelectedItem();
242
243
        if ($item->canSelect()) {
244
            $callable = $item->getSelectAction();
245
            $callable($this);
246
        }
247
    }
248
249
    /**
250
     * Redraw the menu
251
     */
252
    public function redraw()
253
    {
254
        if (!$this->isOpen()) {
255
            throw new MenuNotOpenException;
256
        }
257
258
        $this->draw();
259
    }
260
261
    /**
262
     * Draw the menu to STDOUT
263
     */
264
    protected function draw()
265
    {
266
        $this->terminal->clean();
267
        $this->terminal->moveCursorToTop();
268
269
        $frame = new Frame;
270
271
        $frame->newLine(2);
272
273
        if (is_string($this->title)) {
274
            $frame->addRows($this->drawMenuItem(new LineBreakItem()));
275
            $frame->addRows($this->drawMenuItem(new StaticItem($this->title)));
276
            $frame->addRows($this->drawMenuItem(new LineBreakItem($this->style->getTitleSeparator())));
277
        }
278
279
        array_map(function ($item, $index) use ($frame) {
280
            $frame->addRows($this->drawMenuItem($item, $index === $this->selectedItem));
281
        }, $this->items, array_keys($this->items));
282
283
        $frame->addRows($this->drawMenuItem(new LineBreakItem()));
284
285
        $frame->newLine(2);
286
287
        foreach ($frame->getRows() as $row) {
288
            echo $row;
289
        }
290
291
        $this->currentFrame = $frame;
292
    }
293
294
    /**
295
     * Draw a menu item
296
     *
297
     * @param MenuItemInterface $item
298
     * @param bool|false $selected
299
     * @return array
300
     */
301
    protected function drawMenuItem(MenuItemInterface $item, $selected = false)
302
    {
303
        $rows = $item->getRows($this->style, $selected);
304
305
        $setColour = $selected
306
            ? $this->style->getSelectedSetCode()
307
            : $this->style->getUnselectedSetCode();
308
309
        $unsetColour = $selected
310
            ? $this->style->getSelectedUnsetCode()
311
            : $this->style->getUnselectedUnsetCode();
312
313
        return array_map(function ($row) use ($setColour, $unsetColour) {
314
            return sprintf(
315
                "%s%s%s%s%s%s%s\n\r",
316
                str_repeat(' ', $this->style->getMargin()),
317
                $setColour,
318
                str_repeat(' ', $this->style->getPadding()),
319
                $row,
320
                str_repeat(' ', $this->style->getRightHandPadding(mb_strlen(s::stripAnsiEscapeSequence($row)))),
321
                $unsetColour,
322
                str_repeat(' ', $this->style->getMargin())
323
            );
324
        }, $rows);
325
    }
326
327
    /**
328
     * @throws InvalidTerminalException
329
     */
330
    public function open()
331
    {
332
        if ($this->isOpen()) {
333
            return;
334
        }
335
336
        $this->configureTerminal();
337
        $this->open = true;
338
        $this->display();
339
    }
340
341
    /**
342
     * Close the menu
343
     *
344
     * @throws InvalidTerminalException
345
     */
346
    public function close()
347
    {
348
        $menu = $this;
349
350
        do {
351
            $menu->closeThis();
352
            $menu = $menu->getParent();
353
        } while (null !== $menu);
354
        
355
        $this->tearDownTerminal();
356
    }
357
358
    /**
359
     * @throws InvalidTerminalException
360
     */
361
    public function closeThis()
362
    {
363
        $this->terminal->clean();
364
        $this->terminal->moveCursorToTop();
365
        $this->open = false;
366
    }
367
368
    /**
369
     * @return MenuItemInterface[]
370
     */
371
    public function getItems()
372
    {
373
        return $this->items;
374
    }
375
376
    /**
377
     * @param MenuItemInterface $item
378
     */
379
    public function removeItem(MenuItemInterface $item)
380
    {
381
        $key = array_search($item, $this->items);
382
383
        if (false === $key) {
384
            throw new \InvalidArgumentException('Item does not exist in menu');
385
        }
386
387
        unset($this->items[$key]);
388
        $this->items = array_values($this->items);
389
    }
390
391
    /**
392
     * @return MenuStyle
393
     */
394
    public function getStyle()
395
    {
396
        return $this->style;
397
    }
398
399
    public function getCurrentFrame()
400
    {
401
        return $this->currentFrame;
402
    }
403
404
    /**
405
     * @param string $text
406
     * @return Flash
407
     */
408 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...
409
    {
410
        if (strpos($text, "\n") !== false) {
411
            throw new \InvalidArgumentException;
412
        }
413
414
        $style = (new MenuStyle($this->terminal))
415
            ->setBg('yellow')
416
            ->setFg('red');
417
418
        return new Flash($this, $style, $this->terminal, $text);
419
    }
420
421
    /**
422
     * @param string $text
423
     * @return Confirm
424
     */
425 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...
426
    {
427
        if (strpos($text, "\n") !== false) {
428
            throw new \InvalidArgumentException;
429
        }
430
431
        $style = (new MenuStyle($this->terminal))
432
            ->setBg('yellow')
433
            ->setFg('red');
434
435
        return new Confirm($this, $style, $this->terminal, $text);
436
    }
437
    /**
438
     * @param string $text
439
     * @return YesNo
440
     */
441 View Code Duplication
    public function yesNo($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...
442
    {
443
        if (strpos($text, "\n") !== false) {
444
            throw new \InvalidArgumentException;
445
        }
446
447
        $style = (new MenuStyle($this->terminal))
448
            ->setBg('yellow')
449
            ->setFg('red');
450
451
        return new YesNo($this, $style, $this->terminal, $text);
452
    }
453
}
454