1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace PhpSchool\CliMenu; |
4
|
|
|
|
5
|
|
|
use PhpSchool\CliMenu\Exception\InvalidInstantiationException; |
6
|
|
|
use PhpSchool\CliMenu\Terminal\TerminalFactory; |
7
|
|
|
use PhpSchool\CliMenu\Terminal\TerminalInterface; |
8
|
|
|
|
9
|
|
|
//TODO: B/W fallback |
10
|
|
|
|
11
|
|
|
/** |
12
|
|
|
* Class MenuStyle |
13
|
|
|
* |
14
|
|
|
* @package PhpSchool\CliMenu |
15
|
|
|
* @author Michael Woodward <[email protected]> |
16
|
|
|
*/ |
17
|
|
|
class MenuStyle |
18
|
|
|
{ |
19
|
|
|
/** |
20
|
|
|
* @var TerminalInterface |
21
|
|
|
*/ |
22
|
|
|
protected $terminal; |
23
|
|
|
|
24
|
|
|
/** |
25
|
|
|
* @var string |
26
|
|
|
*/ |
27
|
|
|
protected $fg; |
28
|
|
|
|
29
|
|
|
/** |
30
|
|
|
* @var string |
31
|
|
|
*/ |
32
|
|
|
protected $bg; |
33
|
|
|
|
34
|
|
|
/** |
35
|
|
|
* @var int |
36
|
|
|
*/ |
37
|
|
|
protected $width; |
38
|
|
|
|
39
|
|
|
/** |
40
|
|
|
* @var int |
41
|
|
|
*/ |
42
|
|
|
protected $padding; |
43
|
|
|
|
44
|
|
|
/** |
45
|
|
|
* @var int |
46
|
|
|
*/ |
47
|
|
|
protected $margin; |
48
|
|
|
|
49
|
|
|
/** |
50
|
|
|
* @var int |
51
|
|
|
*/ |
52
|
|
|
protected $contentWidth; |
53
|
|
|
|
54
|
|
|
/** |
55
|
|
|
* @var string |
56
|
|
|
*/ |
57
|
|
|
private $selectedMarker; |
58
|
|
|
|
59
|
|
|
/** |
60
|
|
|
* @var string |
61
|
|
|
*/ |
62
|
|
|
private $unselectedMarker; |
63
|
|
|
|
64
|
|
|
/** |
65
|
|
|
* @var string |
66
|
|
|
*/ |
67
|
|
|
private $itemExtra; |
68
|
|
|
|
69
|
|
|
/** |
70
|
|
|
* @var bool |
71
|
|
|
*/ |
72
|
|
|
private $displaysExtra; |
73
|
|
|
|
74
|
|
|
/** |
75
|
|
|
* @var string |
76
|
|
|
*/ |
77
|
|
|
private $titleSeparator; |
78
|
|
|
|
79
|
|
|
/** |
80
|
|
|
* @var string |
81
|
|
|
*/ |
82
|
|
|
private $allowedConsumer = 'PhpSchool\CliMenu\CliMenuBuilder'; |
83
|
|
|
|
84
|
|
|
/** |
85
|
|
|
* @var array |
86
|
|
|
*/ |
87
|
|
|
private static $availableForegroundColors = array( |
88
|
|
|
'black' => array('set' => 30, 'unset' => 39), |
89
|
|
|
'red' => array('set' => 31, 'unset' => 39), |
90
|
|
|
'green' => array('set' => 32, 'unset' => 39), |
91
|
|
|
'yellow' => array('set' => 33, 'unset' => 39), |
92
|
|
|
'blue' => array('set' => 34, 'unset' => 39), |
93
|
|
|
'magenta' => array('set' => 35, 'unset' => 39), |
94
|
|
|
'cyan' => array('set' => 36, 'unset' => 39), |
95
|
|
|
'white' => array('set' => 37, 'unset' => 39), |
96
|
|
|
'default' => array('set' => 39, 'unset' => 39), |
97
|
|
|
); |
98
|
|
|
|
99
|
|
|
/** |
100
|
|
|
* @var array |
101
|
|
|
*/ |
102
|
|
|
private static $availableBackgroundColors = array( |
103
|
|
|
'black' => array('set' => 40, 'unset' => 49), |
104
|
|
|
'red' => array('set' => 41, 'unset' => 49), |
105
|
|
|
'green' => array('set' => 42, 'unset' => 49), |
106
|
|
|
'yellow' => array('set' => 43, 'unset' => 49), |
107
|
|
|
'blue' => array('set' => 44, 'unset' => 49), |
108
|
|
|
'magenta' => array('set' => 45, 'unset' => 49), |
109
|
|
|
'cyan' => array('set' => 46, 'unset' => 49), |
110
|
|
|
'white' => array('set' => 47, 'unset' => 49), |
111
|
|
|
'default' => array('set' => 49, 'unset' => 49), |
112
|
|
|
); |
113
|
|
|
|
114
|
|
|
/** |
115
|
|
|
* @var array |
116
|
|
|
*/ |
117
|
|
|
private static $availableOptions = array( |
118
|
|
|
'bold' => array('set' => 1, 'unset' => 22), |
119
|
|
|
'dim' => array('set' => 2, 'unset' => 22), |
120
|
|
|
'underscore' => array('set' => 4, 'unset' => 24), |
121
|
|
|
'blink' => array('set' => 5, 'unset' => 25), |
122
|
|
|
'reverse' => array('set' => 7, 'unset' => 27), |
123
|
|
|
'conceal' => array('set' => 8, 'unset' => 28) |
124
|
|
|
); |
125
|
|
|
|
126
|
|
|
/** |
127
|
|
|
* Initialise style |
128
|
|
|
* |
129
|
|
|
* @param string $bg |
130
|
|
|
* @param string $fg |
131
|
|
|
* @param int $width |
132
|
|
|
* @param int $padding |
133
|
|
|
* @param int $margin |
134
|
|
|
* @param string $unselectedMarker |
135
|
|
|
* @param string $selectedMarker |
136
|
|
|
* @param string $itemExtra |
137
|
|
|
* @param bool $displaysExtra |
138
|
|
|
* @param string $titleSeparator |
139
|
|
|
* @param TerminalInterface $terminal |
140
|
|
|
* @throws InvalidInstantiationException |
141
|
|
|
*/ |
142
|
|
|
// TODO: Disabled marker ideally without breaking change. |
143
|
|
|
public function __construct( |
144
|
|
|
$bg = 'blue', |
145
|
|
|
$fg = 'white', |
146
|
|
|
$width = 100, |
147
|
|
|
$padding = 2, |
148
|
|
|
$margin = 2, |
149
|
|
|
$unselectedMarker = '○', |
150
|
|
|
$selectedMarker = '●', |
151
|
|
|
$itemExtra = '✔', |
152
|
|
|
$displaysExtra = false, |
153
|
|
|
$titleSeparator = '=', |
154
|
|
|
TerminalInterface $terminal = null |
155
|
|
|
) { |
156
|
|
|
$builder = debug_backtrace(); |
157
|
|
View Code Duplication |
if (count($builder) < 2 || !isset($builder[1]['class']) || $builder[1]['class'] !== $this->allowedConsumer) { |
|
|
|
|
158
|
|
|
throw new InvalidInstantiationException( |
159
|
|
|
sprintf('The MenuStyle must be instantiated by "%s"', $this->allowedConsumer) |
160
|
|
|
); |
161
|
|
|
} |
162
|
|
|
|
163
|
|
|
$this->terminal = $terminal ?: TerminalFactory::fromSystem(); |
164
|
|
|
$this->bg = $bg; |
165
|
|
|
$this->fg = $fg; |
166
|
|
|
$this->padding = $padding; |
167
|
|
|
$this->margin = $margin; |
168
|
|
|
$this->itemExtra = $itemExtra; |
169
|
|
|
$this->displaysExtra = $displaysExtra; |
170
|
|
|
$this->titleSeparator = $titleSeparator; |
171
|
|
|
|
172
|
|
|
$this->setUnselectedMarker($unselectedMarker); |
173
|
|
|
$this->setSelectedMarker($selectedMarker); |
174
|
|
|
$this->setWidth($width); |
175
|
|
|
$this->calculateContentWidth(); |
176
|
|
|
} |
177
|
|
|
|
178
|
|
|
/** |
179
|
|
|
* @return array |
180
|
|
|
*/ |
181
|
|
|
public static function getAvailableColours() |
182
|
|
|
{ |
183
|
|
|
return array_keys(self::$availableBackgroundColors); |
184
|
|
|
} |
185
|
|
|
|
186
|
|
|
/** |
187
|
|
|
* @param string $text |
188
|
|
|
* @return string |
189
|
|
|
*/ |
190
|
|
|
public function getDisabledItemText($text) |
191
|
|
|
{ |
192
|
|
|
return sprintf( |
193
|
|
|
"\033[%sm%s\033[%sm", |
194
|
|
|
self::$availableOptions['dim']['set'], |
195
|
|
|
$text, |
196
|
|
|
self::$availableOptions['dim']['unset'] |
197
|
|
|
); |
198
|
|
|
} |
199
|
|
|
|
200
|
|
|
/** |
201
|
|
|
* Get the colour code set for Bg and Fg |
202
|
|
|
* |
203
|
|
|
* @return string |
204
|
|
|
*/ |
205
|
|
View Code Duplication |
public function getSelectedSetCode() |
|
|
|
|
206
|
|
|
{ |
207
|
|
|
return sprintf( |
208
|
|
|
"\033[%sm", |
209
|
|
|
implode(';', [ |
210
|
|
|
self::$availableBackgroundColors[$this->getFg()]['set'], |
211
|
|
|
self::$availableForegroundColors[$this->getBg()]['set'], |
212
|
|
|
]) |
213
|
|
|
); |
214
|
|
|
} |
215
|
|
|
|
216
|
|
|
/** |
217
|
|
|
* Get the colour unset code for Bg and Fg |
218
|
|
|
* |
219
|
|
|
* @return string |
220
|
|
|
*/ |
221
|
|
View Code Duplication |
public function getSelectedUnsetCode() |
|
|
|
|
222
|
|
|
{ |
223
|
|
|
return sprintf( |
224
|
|
|
"\033[%sm", |
225
|
|
|
implode(';', [ |
226
|
|
|
self::$availableBackgroundColors[$this->getBg()]['unset'], |
227
|
|
|
self::$availableForegroundColors[$this->getFg()]['unset'], |
228
|
|
|
]) |
229
|
|
|
); |
230
|
|
|
} |
231
|
|
|
|
232
|
|
|
/** |
233
|
|
|
* Get the inverted colour code |
234
|
|
|
* |
235
|
|
|
* @return string |
236
|
|
|
*/ |
237
|
|
View Code Duplication |
public function getUnselectedSetCode() |
|
|
|
|
238
|
|
|
{ |
239
|
|
|
return sprintf( |
240
|
|
|
"\033[%sm", |
241
|
|
|
implode(';', [ |
242
|
|
|
self::$availableBackgroundColors[$this->getBg()]['set'], |
243
|
|
|
self::$availableForegroundColors[$this->getFg()]['set'], |
244
|
|
|
]) |
245
|
|
|
); |
246
|
|
|
} |
247
|
|
|
|
248
|
|
|
/** |
249
|
|
|
* Get the inverted colour unset code |
250
|
|
|
* |
251
|
|
|
* @return string |
252
|
|
|
*/ |
253
|
|
View Code Duplication |
public function getUnselectedUnsetCode() |
|
|
|
|
254
|
|
|
{ |
255
|
|
|
return sprintf( |
256
|
|
|
"\033[%sm", |
257
|
|
|
implode(';', [ |
258
|
|
|
self::$availableBackgroundColors[$this->getBg()]['unset'], |
259
|
|
|
self::$availableForegroundColors[$this->getFg()]['unset'], |
260
|
|
|
]) |
261
|
|
|
); |
262
|
|
|
} |
263
|
|
|
|
264
|
|
|
/** |
265
|
|
|
* Calculate the contents width |
266
|
|
|
*/ |
267
|
|
|
protected function calculateContentWidth() |
268
|
|
|
{ |
269
|
|
|
$this->contentWidth = $this->width - ($this->padding*2) - ($this->margin*2); |
270
|
|
|
} |
271
|
|
|
|
272
|
|
|
/** |
273
|
|
|
* @return string |
274
|
|
|
*/ |
275
|
|
|
public function getFg() |
276
|
|
|
{ |
277
|
|
|
return $this->fg; |
278
|
|
|
} |
279
|
|
|
|
280
|
|
|
/** |
281
|
|
|
* @param string $fg |
282
|
|
|
* @return MenuStyle |
283
|
|
|
*/ |
284
|
|
|
public function setFg($fg) |
285
|
|
|
{ |
286
|
|
|
$this->fg = $fg; |
287
|
|
|
|
288
|
|
|
return $this; |
289
|
|
|
} |
290
|
|
|
|
291
|
|
|
/** |
292
|
|
|
* @return string |
293
|
|
|
*/ |
294
|
|
|
public function getBg() |
295
|
|
|
{ |
296
|
|
|
return $this->bg; |
297
|
|
|
} |
298
|
|
|
|
299
|
|
|
/** |
300
|
|
|
* @param string $bg |
301
|
|
|
* @return MenuStyle |
302
|
|
|
*/ |
303
|
|
|
public function setBg($bg) |
304
|
|
|
{ |
305
|
|
|
$this->bg = $bg; |
306
|
|
|
|
307
|
|
|
return $this; |
308
|
|
|
} |
309
|
|
|
|
310
|
|
|
/** |
311
|
|
|
* @return int |
312
|
|
|
*/ |
313
|
|
|
public function getWidth() |
314
|
|
|
{ |
315
|
|
|
return $this->width; |
316
|
|
|
} |
317
|
|
|
|
318
|
|
|
/** |
319
|
|
|
* @param int $width |
320
|
|
|
* @return MenuStyle |
321
|
|
|
*/ |
322
|
|
|
public function setWidth($width) |
323
|
|
|
{ |
324
|
|
|
$availableWidth = $this->terminal->getWidth() - ($this->margin * 2) - ($this->padding * 2); |
325
|
|
|
|
326
|
|
|
if ($width >= $availableWidth) { |
327
|
|
|
$width = $availableWidth; |
328
|
|
|
} |
329
|
|
|
|
330
|
|
|
$this->width = $width; |
331
|
|
|
$this->calculateContentWidth(); |
332
|
|
|
|
333
|
|
|
return $this; |
334
|
|
|
} |
335
|
|
|
|
336
|
|
|
/** |
337
|
|
|
* @return int |
338
|
|
|
*/ |
339
|
|
|
public function getPadding() |
340
|
|
|
{ |
341
|
|
|
return $this->padding; |
342
|
|
|
} |
343
|
|
|
|
344
|
|
|
/** |
345
|
|
|
* @param int $padding |
346
|
|
|
* @return MenuStyle |
347
|
|
|
*/ |
348
|
|
|
public function setPadding($padding) |
349
|
|
|
{ |
350
|
|
|
$this->padding = $padding; |
351
|
|
|
|
352
|
|
|
$this->calculateContentWidth(); |
353
|
|
|
|
354
|
|
|
return $this; |
355
|
|
|
} |
356
|
|
|
|
357
|
|
|
/** |
358
|
|
|
* @return int |
359
|
|
|
*/ |
360
|
|
|
public function getMargin() |
361
|
|
|
{ |
362
|
|
|
return $this->margin; |
363
|
|
|
} |
364
|
|
|
|
365
|
|
|
/** |
366
|
|
|
* @param int $margin |
367
|
|
|
* @return MenuStyle |
368
|
|
|
*/ |
369
|
|
|
public function setMargin($margin) |
370
|
|
|
{ |
371
|
|
|
$this->margin = $margin; |
372
|
|
|
|
373
|
|
|
$this->calculateContentWidth(); |
374
|
|
|
|
375
|
|
|
return $this; |
376
|
|
|
} |
377
|
|
|
|
378
|
|
|
/** |
379
|
|
|
* @return int |
380
|
|
|
*/ |
381
|
|
|
public function getContentWidth() |
382
|
|
|
{ |
383
|
|
|
return $this->contentWidth; |
384
|
|
|
} |
385
|
|
|
|
386
|
|
|
/** |
387
|
|
|
* Get padding for right had side of content |
388
|
|
|
* |
389
|
|
|
* @param $contentLength |
390
|
|
|
* @return int |
391
|
|
|
*/ |
392
|
|
|
public function getRightHandPadding($contentLength) |
393
|
|
|
{ |
394
|
|
|
return $this->getContentWidth() - $contentLength + $this->getPadding(); |
395
|
|
|
} |
396
|
|
|
|
397
|
|
|
/** |
398
|
|
|
* @return string |
399
|
|
|
*/ |
400
|
|
|
public function getSelectedMarker() |
401
|
|
|
{ |
402
|
|
|
return $this->selectedMarker; |
403
|
|
|
} |
404
|
|
|
|
405
|
|
|
/** |
406
|
|
|
* @param string $marker |
407
|
|
|
* @return $this |
408
|
|
|
*/ |
409
|
|
|
public function setSelectedMarker($marker) |
410
|
|
|
{ |
411
|
|
|
$this->selectedMarker = mb_substr($marker, 0, 1); |
412
|
|
|
|
413
|
|
|
return $this; |
414
|
|
|
} |
415
|
|
|
|
416
|
|
|
/** |
417
|
|
|
* @return string |
418
|
|
|
*/ |
419
|
|
|
public function getUnselectedMarker() |
420
|
|
|
{ |
421
|
|
|
return $this->unselectedMarker; |
422
|
|
|
} |
423
|
|
|
|
424
|
|
|
/** |
425
|
|
|
* @param string $marker |
426
|
|
|
* @return $this |
427
|
|
|
*/ |
428
|
|
|
public function setUnselectedMarker($marker) |
429
|
|
|
{ |
430
|
|
|
$this->unselectedMarker = mb_substr($marker, 0, 1); |
431
|
|
|
|
432
|
|
|
return $this; |
433
|
|
|
} |
434
|
|
|
|
435
|
|
|
/** |
436
|
|
|
* Get the correct marker for the item |
437
|
|
|
* |
438
|
|
|
* @param bool $selected |
439
|
|
|
* @return string |
440
|
|
|
*/ |
441
|
|
|
public function getMarker($selected) |
442
|
|
|
{ |
443
|
|
|
return $selected ? $this->selectedMarker : $this->unselectedMarker; |
444
|
|
|
} |
445
|
|
|
|
446
|
|
|
/** |
447
|
|
|
* @param string $itemExtra |
448
|
|
|
*/ |
449
|
|
|
public function setItemExtra($itemExtra) |
450
|
|
|
{ |
451
|
|
|
$this->itemExtra = $itemExtra; |
452
|
|
|
} |
453
|
|
|
|
454
|
|
|
/** |
455
|
|
|
* @return string |
456
|
|
|
*/ |
457
|
|
|
public function getItemExtra() |
458
|
|
|
{ |
459
|
|
|
return $this->itemExtra; |
460
|
|
|
} |
461
|
|
|
|
462
|
|
|
/** |
463
|
|
|
* @return bool |
464
|
|
|
*/ |
465
|
|
|
public function getDisplaysExtra() |
466
|
|
|
{ |
467
|
|
|
return $this->displaysExtra; |
468
|
|
|
} |
469
|
|
|
|
470
|
|
|
/** |
471
|
|
|
* @param bool $displaysExtra |
472
|
|
|
* @return $this |
473
|
|
|
*/ |
474
|
|
|
public function setDisplaysExtra($displaysExtra) |
475
|
|
|
{ |
476
|
|
|
$this->displaysExtra = $displaysExtra; |
477
|
|
|
|
478
|
|
|
return $this; |
479
|
|
|
} |
480
|
|
|
|
481
|
|
|
/** |
482
|
|
|
* @return string |
483
|
|
|
*/ |
484
|
|
|
public function getTitleSeparator() |
485
|
|
|
{ |
486
|
|
|
return $this->titleSeparator; |
487
|
|
|
} |
488
|
|
|
|
489
|
|
|
/** |
490
|
|
|
* @param string $actionSeparator |
491
|
|
|
* @return $this |
492
|
|
|
*/ |
493
|
|
|
public function setTitleSeparator($actionSeparator) |
494
|
|
|
{ |
495
|
|
|
$this->titleSeparator = $actionSeparator; |
496
|
|
|
|
497
|
|
|
return $this; |
498
|
|
|
} |
499
|
|
|
} |
500
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.