1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace PhpSchool\CliMenu\Input; |
4
|
|
|
|
5
|
|
|
use PhpSchool\CliMenu\CliMenu; |
6
|
|
|
use PhpSchool\CliMenu\MenuStyle; |
7
|
|
|
use PhpSchool\CliMenu\Terminal\TerminalInterface; |
8
|
|
|
use PhpSchool\CliMenu\Util\StringUtil; |
9
|
|
|
|
10
|
|
|
/** |
11
|
|
|
* @author Aydin Hassan <[email protected]> |
12
|
|
|
*/ |
13
|
|
|
class InputIO |
14
|
|
|
{ |
15
|
|
|
/** |
16
|
|
|
* @var MenuStyle |
17
|
|
|
*/ |
18
|
|
|
private $style; |
19
|
|
|
|
20
|
|
|
/** |
21
|
|
|
* @var CliMenu |
22
|
|
|
*/ |
23
|
|
|
private $parentMenu; |
24
|
|
|
|
25
|
|
|
/** |
26
|
|
|
* @var TerminalInterface |
27
|
|
|
*/ |
28
|
|
|
private $terminal; |
29
|
|
|
|
30
|
|
|
/** |
31
|
|
|
* @var array |
32
|
|
|
*/ |
33
|
|
|
private $inputMap = [ |
34
|
|
|
"\n" => 'enter', |
35
|
|
|
"\r" => 'enter', |
36
|
|
|
"\177" => 'backspace' |
37
|
|
|
]; |
38
|
|
|
|
39
|
|
|
/** |
40
|
|
|
* @var callable[][] |
41
|
|
|
*/ |
42
|
|
|
private $callbacks = []; |
43
|
|
|
|
44
|
|
|
public function __construct(CliMenu $parentMenu, MenuStyle $menuStyle, TerminalInterface $terminal) |
45
|
|
|
{ |
46
|
|
|
$this->style = $menuStyle; |
47
|
|
|
$this->terminal = $terminal; |
48
|
|
|
$this->parentMenu = $parentMenu; |
49
|
|
|
} |
50
|
|
|
|
51
|
|
|
public function collect(Input $input) : InputResult |
52
|
|
|
{ |
53
|
|
|
$this->drawInput($input, $input->getPlaceholderText()); |
54
|
|
|
|
55
|
|
|
$inputValue = $input->getPlaceholderText(); |
56
|
|
|
|
57
|
|
|
while (($userInput = $this->terminal->getKeyedInput($this->inputMap)) !== false) { |
|
|
|
|
58
|
|
|
$this->parentMenu->redraw(); |
59
|
|
|
$this->drawInput($input, $inputValue); |
60
|
|
|
|
61
|
|
|
if ($userInput === 'enter') { |
62
|
|
|
if ($input->validate($inputValue)) { |
63
|
|
|
$this->parentMenu->redraw(); |
64
|
|
|
return new InputResult($inputValue); |
65
|
|
|
} else { |
66
|
|
|
$this->drawInputWithError($input, $inputValue); |
67
|
|
|
continue; |
68
|
|
|
} |
69
|
|
|
} |
70
|
|
|
|
71
|
|
|
if ($userInput === 'backspace') { |
72
|
|
|
$inputValue = substr($inputValue, 0, -1); |
73
|
|
|
$this->drawInput($input, $inputValue); |
74
|
|
|
continue; |
75
|
|
|
} |
76
|
|
|
|
77
|
|
|
if (!empty($this->callbacks[$userInput])) { |
78
|
|
|
foreach ($this->callbacks[$userInput] as $callback) { |
79
|
|
|
$inputValue = $callback($this, $inputValue); |
80
|
|
|
$this->drawInput($input, $inputValue); |
81
|
|
|
} |
82
|
|
|
continue; |
83
|
|
|
} |
84
|
|
|
|
85
|
|
|
$inputValue .= $userInput; |
86
|
|
|
$this->drawInput($input, $inputValue); |
87
|
|
|
} |
88
|
|
|
} |
89
|
|
|
|
90
|
|
|
public function registerInputMap(string $input, string $mapTo) : void |
91
|
|
|
{ |
92
|
|
|
$this->inputMap[$input] = $mapTo; |
93
|
|
|
} |
94
|
|
|
|
95
|
|
|
public function registerControlCallback(string $control, callable $callback) : void |
96
|
|
|
{ |
97
|
|
|
if (!isset($this->callbacks[$control])) { |
98
|
|
|
$this->callbacks[$control] = []; |
99
|
|
|
} |
100
|
|
|
|
101
|
|
|
$this->callbacks[$control][] = $callback; |
102
|
|
|
} |
103
|
|
|
|
104
|
|
|
private function getInputWidth(array $lines) |
105
|
|
|
{ |
106
|
|
|
return max( |
107
|
|
|
array_map( |
108
|
|
|
function (string $line) { |
109
|
|
|
return mb_strlen($line); |
110
|
|
|
}, |
111
|
|
|
$lines |
112
|
|
|
) |
113
|
|
|
); |
114
|
|
|
} |
115
|
|
|
|
116
|
|
|
private function calculateYPosition(Input $input) : int |
|
|
|
|
117
|
|
|
{ |
118
|
|
|
$lines = 5; //1. empty 2. prompt text 3. empty 4. input 5. empty |
|
|
|
|
119
|
|
|
|
120
|
|
|
return ceil($this->parentMenu->getCurrentFrame()->count() / 2) - ceil($lines /2) + 1; |
121
|
|
|
} |
122
|
|
|
|
123
|
|
|
private function calculateYPositionWithError() : int |
124
|
|
|
{ |
125
|
|
|
$lines = 7; //1. empty 2. prompt text 3. empty 4. input 5. empty 6. error 7. empty |
|
|
|
|
126
|
|
|
|
127
|
|
|
return ceil($this->parentMenu->getCurrentFrame()->count() / 2) - ceil($lines /2) + 1; |
128
|
|
|
} |
129
|
|
|
|
130
|
|
|
private function calculateXPosition(Input $input, string $userInput) : int |
131
|
|
|
{ |
132
|
|
|
$width = $this->getInputWidth( |
133
|
|
|
[ |
134
|
|
|
$input->getPromptText(), |
135
|
|
|
$input->getValidationFailedText(), |
136
|
|
|
$userInput |
137
|
|
|
] |
138
|
|
|
); |
139
|
|
|
|
140
|
|
|
$parentStyle = $this->parentMenu->getStyle(); |
141
|
|
|
$halfWidth = ($width + ($this->style->getPadding() * 2)) / 2; |
142
|
|
|
$parentHalfWidth = ceil($parentStyle->getWidth() / 2); |
143
|
|
|
|
144
|
|
|
return $parentHalfWidth - $halfWidth; |
145
|
|
|
} |
146
|
|
|
|
147
|
|
|
private function drawLine(Input $input, string $userInput, string $text) : void |
148
|
|
|
{ |
149
|
|
|
$this->terminal->moveCursorToColumn($this->calculateXPosition($input, $userInput)); |
150
|
|
|
|
151
|
|
|
printf( |
152
|
|
|
"%s%s%s%s%s\n", |
153
|
|
|
$this->style->getUnselectedSetCode(), |
154
|
|
|
str_repeat(' ', $this->style->getPadding()), |
155
|
|
|
$text, |
156
|
|
|
str_repeat(' ', $this->style->getPadding()), |
157
|
|
|
$this->style->getUnselectedUnsetCode() |
158
|
|
|
); |
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 = ($width / 2) - ($textLength / 2); |
173
|
|
|
$rightFill = 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($input)); |
207
|
|
|
|
208
|
|
|
$this->drawEmptyLine($input, $userInput); |
209
|
|
|
$this->drawTitle($input, $userInput); |
210
|
|
|
$this->drawEmptyLine($input, $userInput); |
211
|
|
|
$this->drawInputField($input, $input->format($userInput)); |
212
|
|
|
$this->drawEmptyLine($input, $userInput); |
213
|
|
|
} |
214
|
|
|
|
215
|
|
|
private function drawInputWithError(Input $input, string $userInput) : void |
216
|
|
|
{ |
217
|
|
|
$this->terminal->moveCursorToRow($this->calculateYPositionWithError($input)); |
|
|
|
|
218
|
|
|
|
219
|
|
|
$this->drawEmptyLine($input, $userInput); |
220
|
|
|
$this->drawTitle($input, $userInput); |
221
|
|
|
$this->drawEmptyLine($input, $userInput); |
222
|
|
|
$this->drawInputField($input, $input->format($userInput)); |
223
|
|
|
$this->drawEmptyLine($input, $userInput); |
224
|
|
|
$this->drawCenteredLine( |
225
|
|
|
$input, |
226
|
|
|
$userInput, |
227
|
|
|
sprintf( |
228
|
|
|
'%s', |
229
|
|
|
$input->getValidationFailedText() |
230
|
|
|
) |
231
|
|
|
); |
232
|
|
|
$this->drawEmptyLine($input, $userInput); |
233
|
|
|
} |
234
|
|
|
|
235
|
|
|
private function drawTitle(Input $input, string $userInput) : void |
236
|
|
|
{ |
237
|
|
|
|
238
|
|
|
$this->drawCenteredLine( |
239
|
|
|
$input, |
240
|
|
|
$userInput, |
241
|
|
|
$input->getPromptText() |
242
|
|
|
); |
243
|
|
|
} |
244
|
|
|
|
245
|
|
|
private function drawInputField(Input $input, string $userInput) : void |
246
|
|
|
{ |
247
|
|
|
$this->drawCenteredLine( |
248
|
|
|
$input, |
249
|
|
|
$userInput, |
250
|
|
|
sprintf( |
251
|
|
|
'%s%s%s%s%s', |
252
|
|
|
$this->style->getUnselectedUnsetCode(), |
253
|
|
|
$this->style->getSelectedSetCode(), |
254
|
|
|
$userInput, |
255
|
|
|
$this->style->getSelectedUnsetCode(), |
256
|
|
|
$this->style->getUnselectedSetCode() |
257
|
|
|
) |
258
|
|
|
); |
259
|
|
|
} |
260
|
|
|
} |
261
|
|
|
|
This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.
If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.
In this case you can add the
@ignore
PhpDoc annotation to the duplicate definition and it will be ignored.