Passed
Push — master ( 9e3d9f...33a1a9 )
by Aydin
01:50
created

src/Input/InputIO.php (3 issues)

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
        $reader = new NonCanonicalReader($this->terminal);
45
46
        while ($char = $reader->readCharacter()) {
47
            if ($char->isNotControl()) {
48
                if ($havePlaceHolderValue) {
49
                    $inputValue = $char->get();
50
                    $havePlaceHolderValue = false;
51
                } else {
52
                    $inputValue .= $char->get();
53
                }
54
55
                $this->parentMenu->redraw();
56
                $this->drawInput($input, $inputValue);
57
                continue;
58
            }
59
60
            if ($char->isHandledControl()) {
61
                switch ($char->getControl()) {
62
                    case InputCharacter::ENTER:
63
                        if ($input->validate($inputValue)) {
64
                            $this->parentMenu->redraw();
65
                            return new InputResult($inputValue);
66
                        } else {
67
                            $this->drawInputWithError($input, $inputValue);
68
                            continue 2;
69
                        }
70
71
                    case InputCharacter::BACKSPACE:
72
                        $inputValue = substr($inputValue, 0, -1);
73
                        $this->parentMenu->redraw();
74
                        $this->drawInput($input, $inputValue);
75
                        continue 2;
76
                }
77
78
                if (!empty($this->callbacks[$char->getControl()])) {
79
                    foreach ($this->callbacks[$char->getControl()] as $callback) {
80
                        $inputValue = $callback($inputValue);
81
                        $this->drawInput($input, $inputValue);
82
                    }
83
                }
84
            }
85
        }
86
    }
87
88
    public function registerControlCallback(string $control, callable $callback) : void
89
    {
90
        if (!isset($this->callbacks[$control])) {
91
            $this->callbacks[$control] = [];
92
        }
93
94
        $this->callbacks[$control][] = $callback;
95
    }
96
97
    private function getInputWidth(array $lines)
98
    {
99
        return max(
100
            array_map(
101
                function (string $line) {
102
                    return mb_strlen($line);
103
                },
104
                $lines
105
            )
106
        );
107
    }
108
109
    private function calculateYPosition() : int
110
    {
111
        $lines = 5; //1. empty 2. prompt text 3. empty 4. input 5. empty
112
113
        return ceil($this->parentMenu->getCurrentFrame()->count() / 2) - ceil($lines /2) + 1;
0 ignored issues
show
Bug Best Practice introduced by
The expression return ceil($this->paren... - ceil($lines / 2) + 1 returns the type double which is incompatible with the type-hinted return integer.
Loading history...
114
    }
115
116
    private function calculateYPositionWithError() : int
117
    {
118
        $lines = 7; //1. empty 2. prompt text 3. empty 4. input 5. empty 6. error 7. empty
119
120
        return ceil($this->parentMenu->getCurrentFrame()->count() / 2) - ceil($lines /2) + 1;
0 ignored issues
show
Bug Best Practice introduced by
The expression return ceil($this->paren... - ceil($lines / 2) + 1 returns the type double which is incompatible with the type-hinted return integer.
Loading history...
121
    }
122
123
    private function calculateXPosition(Input $input, string $userInput) : int
124
    {
125
        $width = $this->getInputWidth(
126
            [
127
                $input->getPromptText(),
128
                $input->getValidationFailedText(),
129
                $userInput
130
            ]
131
        );
132
133
        $parentStyle     = $this->parentMenu->getStyle();
134
        $halfWidth       = ($width + ($input->getStyle()->getPaddingLeftRight() * 2)) / 2;
135
        $parentHalfWidth = ceil($parentStyle->getWidth() / 2 + $parentStyle->getMargin());
136
137
        return $parentHalfWidth - $halfWidth;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $parentHalfWidth - $halfWidth returns the type double which is incompatible with the type-hinted return integer.
Loading history...
138
    }
139
140
    private function drawLine(Input $input, string $userInput, string $text) : void
141
    {
142
        $this->terminal->moveCursorToColumn($this->calculateXPosition($input, $userInput));
143
144
        $line = sprintf(
145
            "%s%s%s%s%s\n",
146
            $input->getStyle()->getColoursSetCode(),
147
            str_repeat(' ', $input->getStyle()->getPaddingLeftRight()),
148
            $text,
149
            str_repeat(' ', $input->getStyle()->getPaddingLeftRight()),
150
            $input->getStyle()->getColoursResetCode()
151
        );
152
153
        $this->terminal->write($line);
154
    }
155
156
    private function drawCenteredLine(Input $input, string $userInput, string $text) : void
157
    {
158
        $width = $this->getInputWidth(
159
            [
160
                $input->getPromptText(),
161
                $input->getValidationFailedText(),
162
                $userInput
163
            ]
164
        );
165
166
        $textLength = mb_strlen(StringUtil::stripAnsiEscapeSequence($text));
167
        $leftFill   = ($width / 2) - ($textLength / 2);
168
        $rightFill  = ceil($width - $leftFill - $textLength);
169
170
        $this->drawLine(
171
            $input,
172
            $userInput,
173
            sprintf(
174
                '%s%s%s',
175
                str_repeat(' ', $leftFill),
176
                $text,
177
                str_repeat(' ', $rightFill)
178
            )
179
        );
180
    }
181
182
    private function drawEmptyLine(Input $input, string $userInput) : void
183
    {
184
        $width = $this->getInputWidth(
185
            [
186
                $input->getPromptText(),
187
                $input->getValidationFailedText(),
188
                $userInput
189
            ]
190
        );
191
192
        $this->drawLine(
193
            $input,
194
            $userInput,
195
            str_repeat(' ', $width)
196
        );
197
    }
198
199
    private function drawInput(Input $input, string $userInput) : void
200
    {
201
        $this->terminal->moveCursorToRow($this->calculateYPosition());
202
203
        $this->drawEmptyLine($input, $userInput);
204
        $this->drawTitle($input, $userInput);
205
        $this->drawEmptyLine($input, $userInput);
206
        $this->drawInputField($input, $input->filter($userInput));
207
        $this->drawEmptyLine($input, $userInput);
208
    }
209
210
    private function drawInputWithError(Input $input, string $userInput) : void
211
    {
212
        $this->terminal->moveCursorToRow($this->calculateYPositionWithError());
213
214
        $this->drawEmptyLine($input, $userInput);
215
        $this->drawTitle($input, $userInput);
216
        $this->drawEmptyLine($input, $userInput);
217
        $this->drawInputField($input, $input->filter($userInput));
218
        $this->drawEmptyLine($input, $userInput);
219
        $this->drawCenteredLine(
220
            $input,
221
            $userInput,
222
            $input->getValidationFailedText()
223
        );
224
        $this->drawEmptyLine($input, $userInput);
225
    }
226
227
    private function drawTitle(Input $input, string $userInput) : void
228
    {
229
230
        $this->drawCenteredLine(
231
            $input,
232
            $userInput,
233
            $input->getPromptText()
234
        );
235
    }
236
237
    private function drawInputField(Input $input, string $userInput) : void
238
    {
239
        $this->drawCenteredLine(
240
            $input,
241
            $userInput,
242
            sprintf(
243
                '%s%s%s',
244
                $input->getStyle()->getInvertedColoursSetCode(),
245
                $userInput,
246
                $input->getStyle()->getInvertedColoursUnsetCode()
247
            )
248
        );
249
    }
250
}
251