Completed
Push — develop ( fa94ad...c6900b )
by Michał
16:07
created

MultiSelect::manageCursor()   B

Complexity

Conditions 8
Paths 12

Size

Total Lines 26
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 72

Importance

Changes 3
Bugs 1 Features 0
Metric Value
cc 8
eloc 11
c 3
b 1
f 0
nc 12
nop 5
dl 0
loc 26
rs 8.4444
ccs 0
cts 11
cp 0
crap 72
1
<?php
2
3
declare(strict_types=1);
4
5
namespace BlueConsole;
6
7
use Symfony\Component\Console\Style\SymfonyStyle;
8
9
class MultiSelect
10
{
11
    /**
12
     * @todo add help bellow (move up, down, space enter)
13
     * @todo add separators
14
     * @todo count line length and select highest value to replace all chars
15
     * @todo option scroll
16
     * @todo separator (---- by default, optionally message)
17
     * @todo comments on array list
18
     * @todo use different select char & brackets for single select
19
     */
20
    protected const CHARS = [
21
        'enter' => 10,
22
        'space' => 32,
23
        'key_up' => 65,
24
        'key_down' => 66
25
    ];
26
27
    public const MOD_LINE_CHAR = "\033[1A";
28
29
    /**
30
     * @var string
31
     */
32
    protected $selectChar = '<fg=blue>❯</>';
33
34
    /**
35
     * @var string
36
     */
37
    protected $selectedChar = '<info>✓</info>';
38
39
    /**
40
     * @var SymfonyStyle $output
41
     */
42
    protected $output;
43
44
    /**
45
     * @var bool
46
     */
47
    protected $showInfo = true;
48
49
    /**
50
     * @var bool|resource
51
     */
52
    protected $stdin;
53
54
    /**
55
     * @param $output
56
     */
57
    public function __construct(SymfonyStyle $output)
58
    {
59
        $this->output = $output;
60
        $this->stdin = \fopen('php://stdin', 'rb');
61
        system('stty cbreak -echo');
62
    }
63
64
    /**
65
     * @param bool $showInfo
66
     * @return $this
67
     */
68
    public function toggleShowInfo(bool $showInfo): self
69
    {
70
        $this->showInfo = $showInfo;
71
72
        return $this;
73
    }
74
75
    /**
76
     * @param string $char
77
     * @return $this
78
     */
79
    public function setSelectChar(string $char): self
80
    {
81
        $this->selectChar = $char;
82
83
        return $this;
84
    }
85
86
    /**
87
     * @param string $char
88
     * @return $this
89
     */
90
    public function setSelectedChar(string $char): self
91
    {
92
        $this->selectedChar = $char;
93
94
        return $this;
95
    }
96
97
    /**
98
     * @param array $dataList
99
     * @return array
100
     */
101
    public function renderMultiSelect(array $dataList): array
102
    {
103
        return $this->renderList($dataList, false);
104
    }
105
106
    /**
107
     * @param array $dataList
108
     * @return null|int
109
     */
110
    public function renderSingleSelect(array $dataList): ?int
111
    {
112
        $selectedOptions = $this->renderList($dataList, true);
113
114
        $keys = \array_keys($selectedOptions);
115
        return \reset($keys);
116
    }
117
118
    /**
119
     * @param array $dataList
120
     * @param bool $isSingleSelect
121
     * @return array
122
     */
123
    protected function renderList(array $dataList, bool $isSingleSelect): array
124
    {
125
        $selectedOptions = [];
126
        $cursor = 0;
127
        $listSize = \count($dataList);
128
129
        $this->renderBasicList($dataList);
130
131
        while (true) {
132
            $char = \ord(\fgetc($this->stdin));
0 ignored issues
show
Bug introduced by
It seems like $this->stdin can also be of type boolean; however, parameter $handle of fgetc() does only seem to accept resource, 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

132
            $char = \ord(\fgetc(/** @scrutinizer ignore-type */ $this->stdin));
Loading history...
133
134
            if (!\in_array($char, self::CHARS, true)) {
135
                continue;
136
            }
137
138
            if ($char === self::CHARS['enter']) {
139
                $this->renderSelectionInfo($dataList, $selectedOptions);
140
141
                break;
142
            }
143
144
            for ($i = 0; $i < $listSize; $i++) {
145
                echo self::MOD_LINE_CHAR;
146
            }
147
148
            [$cursor, $selectedOptions] = $this->manageCursor(
149
                $cursor,
150
                $char,
151
                $listSize,
152
                $selectedOptions,
153
                $isSingleSelect
154
            );
155
156
            $this->renderListWithSelection($dataList, $cursor, $selectedOptions);
157
158
            \usleep(500000);
159
        }
160
161
        return $selectedOptions;
162
    }
163
164
    /**
165
     * @param int $cursor
166
     * @param int $char
167
     * @param int $listSize
168
     * @param bool $isSingleSelect
169
     * @param array $selectedOptions
170
     * @return array
171
     */
172
    protected function manageCursor(
173
        int $cursor,
174
        int $char,
175
        int $listSize,
176
        array $selectedOptions,
177
        bool $isSingleSelect = false
178
    ): array {
179
        if ($cursor > 0 && $char === self::CHARS['key_up']) {
180
            $cursor--;
181
        }
182
183
        if ($cursor < $listSize - 1 && $char === self::CHARS['key_down']) {
184
            $cursor++;
185
        }
186
187
        if ($char === self::CHARS['space']) {
188
            [$selectedOptions, $oldSelections] = $this->singleSelection($isSingleSelect, $cursor, $selectedOptions);
189
190
            if ($oldSelections || isset($selectedOptions[$cursor])) {
191
                unset($selectedOptions[$cursor]);
192
            } else {
193
                $selectedOptions[$cursor] = true;
194
            }
195
        }
196
197
        return [$cursor, $selectedOptions];
198
    }
199
200
    /**
201
     * @param bool $isSingleSelect
202
     * @param int $cursor
203
     * @param array $selectedOptions
204
     * @return array
205
     */
206
    protected function singleSelection(bool $isSingleSelect, int $cursor, array $selectedOptions): array
207
    {
208
        $oldSelections = false;
209
210
        if ($isSingleSelect) {
211
            if (isset($selectedOptions[$cursor])) {
212
                $oldSelections = true;
213
            }
214
215
            $selectedOptions = [];
216
        }
217
218
        return [$selectedOptions, $oldSelections];
219
    }
220
221
    /**
222
     * @param array $dataList
223
     * @param array $selectedOptions
224
     * @return MultiSelect
225
     */
226
    protected function renderSelectionInfo(array $dataList, array $selectedOptions): self
227
    {
228
        if (!$this->showInfo) {
229
            return $this;
230
        }
231
232
        $this->output->writeln('');
233
        $this->output->title('Selected:');
234
235
        echo self::MOD_LINE_CHAR;
236
237
        foreach ($dataList as $key => $row) {
238
            if (\array_key_exists($key, $selectedOptions)) {
239
                $this->output->writeln("$key: <info>$row</info>");
240
            }
241
        }
242
243
        return $this;
244
    }
245
246
    /**
247
     * @param array $dataList
248
     * @param int $cursor
249
     * @param array $selectedOptions
250
     * @return MultiSelect
251
     */
252
    protected function renderListWithSelection(array $dataList, int $cursor, array $selectedOptions): self
253
    {
254
        foreach ($dataList as $key => $row) {
255
            $cursorChar = ' ';
256
            $selected = '[ ]';
257
258
            if ($cursor === $key) {
259
                $cursorChar = $this->selectChar;
260
            }
261
262
            if ($cursorChar !== ' ') {
263
                $selected = "<fg=blue>$selected</>";
264
            }
265
266
            if (\array_key_exists($key, $selectedOptions)) {
267
                $selected = '[' . $this->selectedChar . ']';
268
            }
269
270
            //@todo resolve colors
271
            if ($cursorChar !== ' ') {
272
                $row = "<fg=blue>$row</>";
273
            } else {
274
                $row = "<comment>$row</comment>";
275
            }
276
277
            $this->output->writeln(" $cursorChar $selected $row");
278
        }
279
280
        return $this;
281
    }
282
283
    /**
284
     * @param array $dataList
285
     * @return MultiSelect
286
     */
287
    protected function renderBasicList(array $dataList): self
288
    {
289
        $count = 0;
290
291
        foreach ($dataList as $key => $row) {
292
            $cursorChar = ' ';
293
294
            if ($key === 0) {
295
                $cursorChar = $this->selectChar;
296
            }
297
298
            if ($count++ === 0) {
299
                 $this->output->writeln(" $cursorChar <fg=blue>[ ]</> <comment>$row</comment>");
300
            } else {
301
                $this->output->writeln(" $cursorChar [ ] <comment>$row</comment>");
302
            }
303
        }
304
305
        return $this;
306
    }
307
}
308