Skip to content

Commit 73adadc

Browse files
authored
Merge pull request #28 from adhocore/develop
Develop
2 parents aafc8c7 + cb70f6c commit 73adadc

File tree

8 files changed

+218
-13
lines changed

8 files changed

+218
-13
lines changed

Diff for: readme.md

+30
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,36 @@ $writer->errorBold('This is error');
345345

346346
// Write a normal raw text.
347347
$writer->raw('Enter name: ');
348+
349+
// Creating tables: just pass array of assoc arrays.
350+
// The keys of first array will be taken as heading.
351+
// Heading is auto inflected to human readable capitalized words (ucwords).
352+
$writer->table([
353+
['a' => 'apple', 'b-c' => 'ball', 'c_d' => 'cat'],
354+
['a' => 'applet', 'b-c' => 'bee', 'c_d' => 'cute'],
355+
]);
356+
357+
// Gives something like:
358+
//
359+
// +--------+------+------+
360+
// | A | B C | C D |
361+
// +--------+------+------+
362+
// | apple | ball | cat |
363+
// | applet | bee | cute |
364+
// +--------+------+------+
365+
366+
// Designing table look and feel: just pass 2nd param $styles.
367+
$writer->table([
368+
['a' => 'apple', 'b-c' => 'ball', 'c_d' => 'cat'],
369+
['a' => 'applet', 'b-c' => 'bee', 'c_d' => 'cute'],
370+
], [
371+
// for => styleName (anything that you would call in $writer instance)
372+
'head' => 'boldGreen', // For the table heading
373+
'odd' => 'bold', // For the odd rows (1st row is odd, then 3, 5 etc)
374+
'even' => 'comment', // For the even rows (2nd row is even, then 4, 6 etc)
375+
]);
376+
// 'head', 'odd', 'even' are all the styles for now
377+
// In future we may support styling a column by its name!
348378
```
349379

350380
#### Reader

Diff for: src/Helper/InflectsString.php

+14
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,18 @@ public function toCamelCase(string $string): string
2727

2828
return \lcfirst($words);
2929
}
30+
31+
/**
32+
* Convert a string to capitalized words.
33+
*
34+
* @param string $string
35+
*
36+
* @return string
37+
*/
38+
public function toWords(string $string): string
39+
{
40+
$words = \trim(\str_replace(['-', '_'], ' ', $string));
41+
42+
return \ucwords($words);
43+
}
3044
}

Diff for: src/Helper/OutputHelper.php

+14-8
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ class OutputHelper
2121
/** @var Writer */
2222
protected $writer;
2323

24+
/** @var int Max width of command name */
25+
protected $maxCmdName;
26+
2427
public function __construct(Writer $writer = null)
2528
{
2629
$this->writer = $writer ?? new Writer;
@@ -63,6 +66,10 @@ public function showOptionsHelp(array $options, string $header = '', string $foo
6366
*/
6467
public function showCommandsHelp(array $commands, string $header = '', string $footer = ''): self
6568
{
69+
$this->maxCmdName = $commands ? \max(\array_map(function (Command $cmd) {
70+
return \strlen($cmd->name());
71+
}, $commands)) : 0;
72+
6673
$this->showHelp('Commands', $commands, $header, $footer);
6774

6875
return $this;
@@ -114,14 +121,13 @@ protected function showHelp(string $for, array $items, string $header = '', stri
114121
*/
115122
protected function sortItems(array $items, &$max = 0): array
116123
{
117-
$first = reset($items);
118-
$max = \strlen($first->name());
119-
120-
\uasort($items, function ($a, $b) use (&$max) {
121-
/** @var Parameter $b */
122-
/** @var Parameter $a */
123-
$max = \max(\strlen($this->getName($a)), \strlen($this->getName($b)), $max);
124+
$max = \max(\array_map(function ($item) {
125+
return \strlen($this->getName($item));
126+
}, $items));
124127

128+
\uasort($items, function ($a, $b) {
129+
/* @var Parameter $b */
130+
/* @var Parameter $a */
125131
return $a->name() <=> $b->name();
126132
});
127133

@@ -140,7 +146,7 @@ protected function getName($item): string
140146
$name = $item->name();
141147

142148
if ($item instanceof Command) {
143-
return \trim($item->alias() . '|' . $name, '|');
149+
return \trim(\str_pad($name, $this->maxCmdName) . ' ' . $item->alias());
144150
}
145151

146152
return $this->label($item);

Diff for: src/IO/Interactor.php

+3
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@
140140
* @method Writer redBgPurple($text, $eol = false)
141141
* @method Writer redBgWhite($text, $eol = false)
142142
* @method Writer redBgYellow($text, $eol = false)
143+
* @method Writer table(array $rows, array $styles = [])
143144
* @method Writer warn($text, $eol = false)
144145
* @method Writer white($text, $eol = false)
145146
* @method Writer yellow($text, $eol = false)
@@ -334,6 +335,8 @@ protected function promptOptions(array $choices, $default): self
334335
$options .= "/<$style>$choice</end>";
335336
}
336337

338+
$options = \ltrim($options, '/');
339+
337340
$this->writer->colors(" ($options): ");
338341

339342
return $this;

Diff for: src/Output/Writer.php

+97
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
namespace Ahc\Cli\Output;
44

5+
use Ahc\Cli\Exception\InvalidArgumentException;
6+
use Ahc\Cli\Helper\InflectsString;
7+
58
/**
69
* Cli Writer.
710
*
@@ -149,6 +152,8 @@
149152
*/
150153
class Writer
151154
{
155+
use InflectsString;
156+
152157
/** @var resource Output file handle */
153158
protected $stream;
154159

@@ -267,6 +272,98 @@ public function raw($text, bool $error = false): self
267272
return $this->doWrite((string) $text, $error);
268273
}
269274

275+
/**
276+
* Generate table for the console. Keys of first row are taken as header.
277+
*
278+
* @param array[] $rows Array of assoc arrays.
279+
* @param array $styles Eg: ['head' => 'bold', 'odd' => 'comment', 'even' => 'green']
280+
*
281+
* @return self
282+
*/
283+
public function table(array $rows, array $styles = []): self
284+
{
285+
if ([] === $table = $this->normalizeTable($rows)) {
286+
return $this;
287+
}
288+
289+
list($head, $rows) = $table;
290+
291+
$styles = $this->normalizeStyles($styles);
292+
$title = $body = $dash = [];
293+
294+
list($start, $end) = $styles['head'];
295+
foreach ($head as $col => $size) {
296+
$dash[] = \str_repeat('-', $size + 2);
297+
$title[] = \str_pad($this->toWords($col), $size, ' ');
298+
}
299+
300+
$title = "|$start " . \implode(" $end|$start ", $title) . " $end|" . \PHP_EOL;
301+
302+
$odd = true;
303+
foreach ($rows as $row) {
304+
$parts = [];
305+
306+
list($start, $end) = $styles[['even', 'odd'][(int) $odd]];
307+
foreach ($head as $col => $size) {
308+
$parts[] = \str_pad($row[$col] ?? '', $size, ' ');
309+
}
310+
311+
$odd = !$odd;
312+
$body[] = "|$start " . \implode(" $end|$start ", $parts) . " $end|";
313+
}
314+
315+
$dash = '+' . \implode('+', $dash) . '+' . \PHP_EOL;
316+
$body = \implode(\PHP_EOL, $body) . \PHP_EOL;
317+
318+
return $this->colors("$dash$title$dash$body$dash");
319+
}
320+
321+
protected function normalizeTable(array $rows): array
322+
{
323+
$head = \reset($rows);
324+
if (empty($head)) {
325+
return [];
326+
}
327+
328+
if (!\is_array($head)) {
329+
throw new InvalidArgumentException(
330+
\sprintf('Rows must be array of assoc arrays, %s given', \gettype($head))
331+
);
332+
}
333+
334+
$head = \array_fill_keys(\array_keys($head), null);
335+
foreach ($rows as $i => &$row) {
336+
$row = \array_merge($head, $row);
337+
}
338+
339+
foreach ($head as $col => &$value) {
340+
$cols = \array_column($rows, $col);
341+
$span = \array_map('strlen', $cols);
342+
$span[] = \strlen($col);
343+
$value = \max($span);
344+
}
345+
346+
return [$head, $rows];
347+
}
348+
349+
protected function normalizeStyles(array $styles)
350+
{
351+
$default = [
352+
// styleFor => ['styleStartFn', 'end']
353+
'head' => ['', ''],
354+
'odd' => ['', ''],
355+
'even' => ['', ''],
356+
];
357+
358+
foreach ($styles as $for => $style) {
359+
if (isset($default[$for])) {
360+
$default[$for] = ['<' . \trim($style, '<> ') . '>', '</end>'];
361+
}
362+
}
363+
364+
return $default;
365+
}
366+
270367
/**
271368
* Write to stdout or stderr magically.
272369
*

Diff for: tests/CliTestCase.php

+14-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
*/
1010
class CliTestCase extends TestCase
1111
{
12+
protected static $ou = __DIR__ . '/output';
13+
1214
public static function setUpBeforeClass()
1315
{
1416
// Thanks: https://stackoverflow.com/a/39785995
@@ -21,16 +23,27 @@ public function setUp()
2123
{
2224
ob_start();
2325
StreamInterceptor::$buffer = '';
26+
file_put_contents(static::$ou, '');
2427
}
2528

2629
public function tearDown()
2730
{
2831
ob_end_clean();
2932
}
3033

34+
public static function tearDownAfterClass()
35+
{
36+
unlink(static::$ou);
37+
}
38+
3139
public function buffer()
3240
{
33-
return StreamInterceptor::$buffer;
41+
return StreamInterceptor::$buffer ?: file_get_contents(static::$ou);
42+
}
43+
44+
public function assertBufferContains($expect)
45+
{
46+
$this->assertContains($expect, $this->buffer());
3447
}
3548
}
3649

Diff for: tests/Helper/InflectsStringTest.php

+7
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,11 @@ public function test_to_camel_case()
1515
$this->assertSame('theLongName', $this->toCamelCase('--the_long-name'));
1616
$this->assertSame('aBC', $this->toCamelCase('a_bC'));
1717
}
18+
19+
public function test_to_words()
20+
{
21+
$this->assertSame('A B', $this->toWords('a-b'));
22+
$this->assertSame('The Long Name', $this->toWords('--the_long-name'));
23+
$this->assertSame('A BC', $this->toWords('a_bC'));
24+
}
1825
}

Diff for: tests/Output/WriterTest.php

+39-4
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public function test_write_bold_red_bggreen()
4242

4343
public function test_raw()
4444
{
45-
$w = new Writer($ou = __DIR__ . '/output');
45+
$w = new Writer(static::$ou);
4646

4747
$w->raw(new class {
4848
public function __toString()
@@ -51,10 +51,45 @@ public function __toString()
5151
}
5252
})->clear();
5353

54-
$out = file_get_contents($ou);
55-
$this->assertSame("__toString\e[2J", $out);
54+
$this->assertSame("__toString\e[2J", $this->buffer());
55+
}
56+
57+
public function test_empty_table()
58+
{
59+
$w = new Writer(static::$ou);
60+
61+
$w->table([]);
62+
63+
$this->assertSame('', $this->buffer(), 'empty');
64+
}
65+
66+
public function test_table()
67+
{
68+
$w = new Writer(static::$ou);
69+
70+
$w->table([
71+
['a' => 'apple', 'b-c' => 'ball', 'c_d' => 'cat'],
72+
['a' => 'applet', 'b-c' => 'bee', 'c_d' => 'cute'],
73+
], [
74+
'head' => 'boldBgGreen',
75+
'odd' => 'purple',
76+
'even' => 'cyan',
77+
]);
78+
79+
$this->assertSame(3, \substr_count($this->buffer(), '+--------+------+------+'), '3 dashes');
80+
$this->assertBufferContains("|\33[1;37;42m A \33[0m|\33[1;37;42m B C \33[0m|\33[1;37;42m C D \33[0m|", 'Head');
81+
$this->assertBufferContains("|\33[0;35m apple \33[0m|\33[0;35m ball \33[0m|\33[0;35m cat \33[0m|", 'Odd');
82+
$this->assertBufferContains("|\33[0;36m applet \33[0m|\33[0;36m bee \33[0m|\33[0;36m cute \33[0m|", 'Even');
83+
}
84+
85+
public function test_table_throws()
86+
{
87+
$w = new Writer(static::$ou);
88+
89+
$this->expectException(\InvalidArgumentException::class);
90+
$this->expectExceptionMessage('Rows must be array of assoc arrays');
5691

57-
unlink($ou);
92+
$w->table([1, 2]);
5893
}
5994

6095
public function test_colorizer()

0 commit comments

Comments
 (0)