Completed
Push — master ( 03739e...51f188 )
by Michał
14:48 queued 08:49
created

MultiSelect::renderListWithSelection()   A

Complexity

Conditions 6
Paths 17

Size

Total Lines 29
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 6
eloc 15
c 2
b 0
f 0
nc 17
nop 3
dl 0
loc 29
rs 9.2222
ccs 0
cts 15
cp 0
crap 42
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
            if (!$this->stdin) {
133
                continue;
134
            }
135
136
            $char = \ord(\fgetc($this->stdin));
0 ignored issues
show
Bug introduced by
It seems like $this->stdin can also be of type true; 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

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