Issues (4)

src/Input/InputIO.php (1 issue)

1
<?php
2
3
namespace PhpSchool\CliMenu\Input;
4
5
use PhpSchool\CliMenu\CliMenu;
6
use PhpSchool\CliMenu\Util\StringUtil;
7
use PhpSchool\Terminal\InputCharacter;
8
use PhpSchool\Terminal\NonCanonicalReader;
9
use PhpSchool\Terminal\Terminal;
10
11
/**
12
 * @author Aydin Hassan <[email protected]>
13
 */
14
class InputIO
15
{
16
    /**
17
     * @var CliMenu
18
     */
19
    private $parentMenu;
20
21
    /**
22
     * @var Terminal
23
     */
24
    private $terminal;
25
26
    /**
27
     * @var callable[][]
28
     */
29
    private $callbacks = [];
30
31
    public function __construct(CliMenu $parentMenu, Terminal $terminal)
32
    {
33
        $this->terminal     = $terminal;
34
        $this->parentMenu   = $parentMenu;
35
    }
36
37
    public function collect(Input $input) : InputResult
38
    {
39
        $this->drawInput($input, $input->getPlaceholderText());
40
41
        $inputValue = $input->getPlaceholderText();
42
        $havePlaceHolderValue = !empty($inputValue);
43
        
44
        $originalValue = $inputValue;
45
46
        $reader = new NonCanonicalReader($this->terminal);
47
48
        while ($char = $reader->readCharacter()) {
49
            if ($char->isNotControl()) {
50
                if ($havePlaceHolderValue) {
51
                    $inputValue = $char->get();
52
                    $havePlaceHolderValue = false;
53
                } else {
54
                    $inputValue .= $char->get();
55
                }
56
57
                $this->parentMenu->redraw();
58
                $this->drawInput($input, $inputValue);
59
                continue;
60
            }
61
62
            if ($char->isHandledControl()) {
63
                switch ($char->getControl()) {
64
                    case InputCharacter::ESC:
65
                        $this->parentMenu->redraw();
66
                        return new InputResult($originalValue);
67
                    case InputCharacter::ENTER:
68
                        if ($input->validate($inputValue)) {
69
                            $this->parentMenu->redraw();
70
                            return new InputResult($inputValue);
71
                        } else {
72
                            $this->drawInputWithError($input, $inputValue);
73
                            continue 2;
74
                        }
75
76
                    case InputCharacter::BACKSPACE:
77
                        $inputValue = substr($inputValue, 0, -1);
78
                        $this->parentMenu->redraw();
79
                        $this->drawInput($input, $inputValue);
80
                        continue 2;
81
                }
82
83
                if (!empty($this->callbacks[$char->getControl()])) {
84
                    foreach ($this->callbacks[$char->getControl()] as $callback) {
85
                        $inputValue = $callback($inputValue);
86
                        $this->drawInput($input, $inputValue);
87
                    }
88
                }
89
            }
90
        }
0 ignored issues
show
Bug Best Practice introduced by
In this branch, the function will implicitly return null which is incompatible with the type-hinted return PhpSchool\CliMenu\Input\InputResult. Consider adding a return statement or allowing null as return value.

For hinted functions/methods where all return statements with the correct type are only reachable via conditions, ?null? gets implicitly returned which may be incompatible with the hinted type. Let?s take a look at an example:

interface ReturnsInt {
    public function returnsIntHinted(): int;
}

class MyClass implements ReturnsInt {
    public function returnsIntHinted(): int
    {
        if (foo()) {
            return 123;
        }
        // here: null is implicitly returned
    }
}
Loading history...
91
    }
92
93
    public function registerControlCallback(string $control, callable $callback) : void
94
    {
95
        if (!isset($this->callbacks[$control])) {
96
            $this->callbacks[$control] = [];
97
        }
98
99
        $this->callbacks[$control][] = $callback;
100
    }
101
102
    private function getInputWidth(array $lines) : int
103
    {
104
        return max(
105
            array_map(
106
                function (string $line) {
107
                    return mb_strlen($line);
108
                },
109
                $lines
110
            )
111
        );
112
    }
113
114
    private function calculateYPosition() : int
115
    {
116
        $lines = 5; //1. empty 2. prompt text 3. empty 4. input 5. empty
117
118
        return (int) (ceil($this->parentMenu->getCurrentFrame()->count() / 2) - ceil($lines /2) + 1);
119
    }
120
121
    private function calculateYPositionWithError() : int
122
    {
123
        $lines = 7; //1. empty 2. prompt text 3. empty 4. input 5. empty 6. error 7. empty
124
125
        return (int) (ceil($this->parentMenu->getCurrentFrame()->count() / 2) - ceil($lines /2) + 1);
126
    }
127
128
    private function calculateXPosition(Input $input, string $userInput) : int
129
    {
130
        $width = $this->getInputWidth(
131
            [
132
                $input->getPromptText(),
133
                $input->getValidationFailedText(),
134
                $userInput
135
            ]
136
        );
137
138
        $parentStyle     = $this->parentMenu->getStyle();
139
        $halfWidth       = ($width + ($input->getStyle()->getPaddingLeftRight() * 2)) / 2;
140
        $parentHalfWidth = ceil($parentStyle->getWidth() / 2 + $parentStyle->getMargin());
141
142
        return (int) ($parentHalfWidth - $halfWidth);
143
    }
144
145
    private function drawLine(Input $input, string $userInput, string $text) : void
146
    {
147
        $this->terminal->moveCursorToColumn($this->calculateXPosition($input, $userInput));
148
149
        $line = sprintf(
150
            "%s%s%s%s%s\n",
151
            $input->getStyle()->getColoursSetCode(),
152
            str_repeat(' ', $input->getStyle()->getPaddingLeftRight()),
153
            $text,
154
            str_repeat(' ', $input->getStyle()->getPaddingLeftRight()),
155
            $input->getStyle()->getColoursResetCode()
156
        );
157
158
        $this->terminal->write($line);
159
    }
160
161
    private function drawCenteredLine(Input $input, string $userInput, string $text) : void
162
    {
163
        $width = $this->getInputWidth(
164
            [
165
                $input->getPromptText(),
166
                $input->getValidationFailedText(),
167
                $userInput
168
            ]
169
        );
170
171
        $textLength = mb_strlen(StringUtil::stripAnsiEscapeSequence($text));
172
        $leftFill   = (int) (($width / 2) - ($textLength / 2));
173
        $rightFill  = (int) ceil($width - $leftFill - $textLength);
174
175
        $this->drawLine(
176
            $input,
177
            $userInput,
178
            sprintf(
179
                '%s%s%s',
180
                str_repeat(' ', $leftFill),
181
                $text,
182
                str_repeat(' ', $rightFill)
183
            )
184
        );
185
    }
186
187
    private function drawEmptyLine(Input $input, string $userInput) : void
188
    {
189
        $width = $this->getInputWidth(
190
            [
191
                $input->getPromptText(),
192
                $input->getValidationFailedText(),
193
                $userInput
194
            ]
195
        );
196
197
        $this->drawLine(
198
            $input,
199
            $userInput,
200
            str_repeat(' ', $width)
201
        );
202
    }
203
204
    private function drawInput(Input $input, string $userInput) : void
205
    {
206
        $this->terminal->moveCursorToRow($this->calculateYPosition());
207
208
        $this->drawEmptyLine($input, $userInput);
209
        $this->drawTitle($input, $userInput);
210
        $this->drawEmptyLine($input, $userInput);
211
        $this->drawInputField($input, $input->filter($userInput));
212
        $this->drawEmptyLine($input, $userInput);
213
    }
214
215
    private function drawInputWithError(Input $input, string $userInput) : void
216
    {
217
        $this->terminal->moveCursorToRow($this->calculateYPositionWithError());
218
219
        $this->drawEmptyLine($input, $userInput);
220
        $this->drawTitle($input, $userInput);
221
        $this->drawEmptyLine($input, $userInput);
222
        $this->drawInputField($input, $input->filter($userInput));
223
        $this->drawEmptyLine($input, $userInput);
224
        $this->drawCenteredLine(
225
            $input,
226
            $userInput,
227
            $input->getValidationFailedText()
228
        );
229
        $this->drawEmptyLine($input, $userInput);
230
    }
231
232
    private function drawTitle(Input $input, string $userInput) : void
233
    {
234
235
        $this->drawCenteredLine(
236
            $input,
237
            $userInput,
238
            $input->getPromptText()
239
        );
240
    }
241
242
    private function drawInputField(Input $input, string $userInput) : void
243
    {
244
        $this->drawCenteredLine(
245
            $input,
246
            $userInput,
247
            sprintf(
248
                '%s%s%s',
249
                $input->getStyle()->getInvertedColoursSetCode(),
250
                $userInput,
251
                $input->getStyle()->getInvertedColoursUnsetCode()
252
            )
253
        );
254
    }
255
}
256