Skip to content

Commit 4545435

Browse files
committed
Introduce a inspect:ui command
1 parent 21fa98a commit 4545435

35 files changed

+2289
-1
lines changed

composer.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@
2222
"veewee/xml": "^3.0",
2323
"azjezz/psl": "^3.0",
2424
"symfony/console": "^5.4 || ^6.0 || ^7.0",
25-
"webmozart/assert": "^1.11"
25+
"webmozart/assert": "^1.11",
26+
"php-tui/php-tui": "^0.2.1"
2627
},
2728
"require-dev": {
2829
"symfony/var-dumper": "^6.1 || ^7.0",
+101
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Soap\WsdlReader\Console\Command;
5+
6+
use PhpTui\Term\Actions;
7+
use PhpTui\Term\ClearType;
8+
use PhpTui\Term\Terminal;
9+
use PhpTui\Tui\Bridge\PhpTerm\PhpTermBackend;
10+
use PhpTui\Tui\Display\Display;
11+
use PhpTui\Tui\DisplayBuilder;
12+
use Soap\Wsdl\Console\Helper\ConfiguredLoader;
13+
use Soap\Wsdl\Loader\CallbackLoader;
14+
use Soap\Wsdl\Loader\FlatteningLoader;
15+
use Soap\Wsdl\Loader\WsdlLoader;
16+
use Soap\WsdlReader\Console\UI\Components\LoadingWidget;
17+
use Soap\WsdlReader\Console\UI\Layout;
18+
use Soap\WsdlReader\Console\UI\UIState;
19+
use Symfony\Component\Console\Command\Command;
20+
use Symfony\Component\Console\Exception\InvalidArgumentException;
21+
use Symfony\Component\Console\Input\InputArgument;
22+
use Symfony\Component\Console\Input\InputInterface;
23+
use Symfony\Component\Console\Input\InputOption;
24+
use Symfony\Component\Console\Output\OutputInterface;
25+
use function Psl\Type\non_empty_string;
26+
27+
final class InspectUICommand extends Command
28+
{
29+
public static function getDefaultName(): string
30+
{
31+
return 'inspect:ui';
32+
}
33+
34+
/**
35+
* @throws InvalidArgumentException
36+
*/
37+
protected function configure(): void
38+
{
39+
$this->setDescription('Inspects WSDL file through a user interface.');
40+
$this->addArgument('wsdl', InputArgument::REQUIRED, 'Provide the URI of the WSDL you want to validate');
41+
$this->addOption('loader', 'l', InputOption::VALUE_REQUIRED, 'Customize the WSDL loader file that will be used');
42+
}
43+
44+
protected function execute(InputInterface $input, OutputInterface $output): int
45+
{
46+
$terminal = Terminal::new();
47+
$terminal->execute(Actions::alternateScreenEnable());
48+
$terminal->execute(Actions::enableMouseCapture());
49+
$terminal->execute(Actions::cursorHide());
50+
$terminal->enableRawMode();
51+
52+
$display = DisplayBuilder::default(PhpTermBackend::new($terminal))->build();
53+
$state = $this->loadState($input, $display);
54+
55+
try {
56+
while ($state->running) {
57+
while (null !== $event = $terminal->events()->next()) {
58+
$state->handle($event);
59+
}
60+
61+
$display->draw(Layout::create($state));
62+
63+
usleep(50_000);
64+
}
65+
} finally {
66+
$terminal->disableRawMode();
67+
$terminal->execute(Actions::alternateScreenDisable());
68+
$terminal->execute(Actions::disableMouseCapture());
69+
$terminal->execute(Actions::cursorShow());
70+
$terminal->execute(Actions::clear(ClearType::All));
71+
}
72+
73+
return self::SUCCESS;
74+
}
75+
76+
private function loadState(InputInterface $input, Display $display): UIState
77+
{
78+
$wsdl = non_empty_string()->assert($input->getArgument('wsdl'));
79+
$info = ['Loading WSDL ...'];
80+
$display->draw(LoadingWidget::create($info));
81+
82+
$loader = ConfiguredLoader::createFromConfig(
83+
$input->getOption('loader'),
84+
static fn (WsdlLoader $loader) => new FlatteningLoader(
85+
new CallbackLoader(static function (string $location) use ($loader, $display, &$info): string {
86+
$info[] = '> Loading '.$location.' ...';
87+
$currentIndex = count($info) - 1;
88+
$display->draw(LoadingWidget::create($info));
89+
90+
$result = $loader($location);
91+
92+
$info[$currentIndex] .= ' OK';
93+
94+
return $result;
95+
})
96+
),
97+
);
98+
99+
return UIState::load($wsdl, $loader);
100+
}
101+
}

src/Console/UI/Component.php

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Soap\WsdlReader\Console\UI;
4+
5+
use PhpTui\Tui\Widget\Widget;
6+
7+
interface Component extends EventHandler
8+
{
9+
public function build(): Widget;
10+
}

src/Console/UI/Components/Help.php

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Soap\WsdlReader\Console\UI\Components;
4+
5+
use PhpTui\Tui\Extension\Core\Widget\BlockWidget;
6+
use PhpTui\Tui\Extension\Core\Widget\TabsWidget;
7+
use PhpTui\Tui\Style\Style;
8+
use PhpTui\Tui\Text\Line;
9+
use PhpTui\Tui\Widget\Borders;
10+
use PhpTui\Tui\Widget\Widget;
11+
12+
final class Help
13+
{
14+
public static function create(Line ... $lines): Widget
15+
{
16+
return BlockWidget::default()
17+
->borders(Borders::ALL)->style(Style::default()->white())
18+
->widget(
19+
TabsWidget::fromTitles(
20+
...$lines
21+
)
22+
);
23+
}
24+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Soap\WsdlReader\Console\UI\Components;
4+
5+
use PhpTui\Tui\Extension\Core\Widget\List\ListItem;
6+
use PhpTui\Tui\Extension\Core\Widget\ListWidget;
7+
use PhpTui\Tui\Style\Style;
8+
use PhpTui\Tui\Text\Text;
9+
use PhpTui\Tui\Widget\Widget;
10+
use function Psl\Vec\map;
11+
12+
final readonly class LoadingWidget
13+
{
14+
/**
15+
* @param list<string> $messages
16+
*/
17+
public static function create(array $messages): Widget
18+
{
19+
return ListWidget::default()
20+
->items(
21+
...map(
22+
$messages,
23+
static fn (string $message) => ListItem::new(Text::fromString($message))
24+
)
25+
)
26+
->select(count($messages) - 1)
27+
->highlightStyle(Style::default()->white()->onBlue());
28+
}
29+
}
+84
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Soap\WsdlReader\Console\UI\Components;
4+
5+
use PhpTui\Tui\Extension\Core\Widget\Table\TableCell;
6+
use PhpTui\Tui\Extension\Core\Widget\Table\TableRow;
7+
use PhpTui\Tui\Extension\Core\Widget\TableWidget;
8+
use PhpTui\Tui\Layout\Constraint;
9+
use PhpTui\Tui\Style\Style;
10+
use PhpTui\Tui\Text\Line;
11+
use PhpTui\Tui\Text\Text;
12+
use PhpTui\Tui\Widget\Widget;
13+
use Psl\Option\Option;
14+
use ReflectionClass;
15+
use ReflectionProperty;
16+
use Throwable;
17+
use function Psl\Dict\filter_nulls;
18+
use function Psl\Vec\map;
19+
20+
final readonly class MetaTable
21+
{
22+
public static function create(object $meta): Widget
23+
{
24+
$headerStyle = Style::default()->bold();
25+
26+
return TableWidget::default()
27+
->widths(
28+
Constraint::min(50),
29+
Constraint::percentage(100),
30+
)
31+
32+
->header(
33+
TableRow::fromCells(
34+
new TableCell(Text::fromLine(Line::fromString('Key')), $headerStyle),
35+
new TableCell(Text::fromLine(Line::fromString('Value')), $headerStyle),
36+
)
37+
)
38+
->rows(...map(
39+
self::buildKeyPairs($meta),
40+
static fn ($current) => TableRow::fromCells(
41+
TableCell::fromString($current[0]),
42+
TableCell::fromString($current[1]),
43+
)
44+
));
45+
}
46+
47+
/**
48+
* @return array<array{0: non-empty-string, 1: string}>
49+
*/
50+
private static function buildKeyPairs(object $object): array
51+
{
52+
$rc = new ReflectionClass($object);
53+
54+
return filter_nulls(
55+
map(
56+
$rc->getProperties(),
57+
/** @return array{0: non-empty-string, 1: string}|null */
58+
static function (ReflectionProperty $prop) use ($object): ?array {
59+
$value = self::tryStringifyValue($prop->getValue($object));
60+
if ($value === null) {
61+
return null;
62+
}
63+
64+
return [$prop->getName(), $value];
65+
}
66+
)
67+
);
68+
}
69+
70+
private static function tryStringifyValue(mixed $value): ?string
71+
{
72+
try {
73+
return match (true) {
74+
is_array($value) => json_encode($value),
75+
is_bool($value) => $value ? 'true' : 'false',
76+
is_scalar($value) => (string)$value,
77+
$value instanceof Option => $value->map(self::tryStringifyValue(...))->unwrapOr(null),
78+
default => null,
79+
};
80+
} catch (Throwable) {
81+
return null;
82+
}
83+
}
84+
}
+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Soap\WsdlReader\Console\UI\Components;
4+
5+
use PhpTui\Tui\Extension\Core\Widget\BlockWidget;
6+
use PhpTui\Tui\Extension\Core\Widget\TabsWidget;
7+
use PhpTui\Tui\Style\Style;
8+
use PhpTui\Tui\Text\Line;
9+
use PhpTui\Tui\Widget\Borders;
10+
use PhpTui\Tui\Widget\Widget;
11+
use Soap\WsdlReader\Console\UI\Page;
12+
use Soap\WsdlReader\Console\UI\UIState;
13+
use function Psl\Vec\keys;
14+
use function Psl\Vec\map;
15+
16+
final class Navigation
17+
{
18+
public static function create(UIState $state): Widget
19+
{
20+
return BlockWidget::default()
21+
->borders(Borders::ALL)->style(Style::default()->white())
22+
->widget(
23+
TabsWidget::fromTitles(
24+
Line::parse('<fg=red>[q]</>uit'),
25+
...map(
26+
$state->availablePages,
27+
static fn (Page $page) => Line::parse($page->title())
28+
)
29+
)
30+
->select((int)array_search(get_class($state->currentPage), keys($state->availablePages), true) + 1)
31+
->highlightStyle(Style::default()->white()->onBlue())
32+
);
33+
}
34+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Soap\WsdlReader\Console\UI\Components;
4+
5+
use PhpTui\Tui\Extension\Core\Widget\ParagraphWidget;
6+
use PhpTui\Tui\Widget\Widget;
7+
8+
final class ScrollableTextArea
9+
{
10+
public static function create(ScrollableTextAreaState $state): Widget
11+
{
12+
$paragraph = ParagraphWidget::fromString($state->value);
13+
$paragraph->scroll = [$state->position, 0];
14+
15+
return $paragraph;
16+
}
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Soap\WsdlReader\Console\UI\Components;
4+
5+
use PhpTui\Term\Event;
6+
use PhpTui\Term\MouseEventKind;
7+
use Soap\WsdlReader\Console\UI\EventHandler;
8+
9+
use function json_encode;
10+
use function Psl\Math\max;
11+
use function Psl\Math\min;
12+
13+
final class ScrollableTextAreaState implements EventHandler
14+
{
15+
public function __construct(
16+
public string $value,
17+
public int $position = 0,
18+
) {
19+
}
20+
21+
public static function json(mixed $data, string $fallback): self
22+
{
23+
return new self(
24+
$data !== null ? json_encode($data, JSON_PRETTY_PRINT) : $fallback,
25+
);
26+
}
27+
28+
public function scrollUp(): void
29+
{
30+
$this->position = (int) max([0, $this->position - 1]);
31+
}
32+
33+
public function scrollDown(): void
34+
{
35+
$this->position = (int) min([$this->position + 1, count(explode("\n", $this->value))]);
36+
}
37+
38+
public function handle(Event $event): void
39+
{
40+
if ($event instanceof Event\MouseEvent) {
41+
match ($event->kind) {
42+
MouseEventKind::ScrollUp => $this->scrollUp(),
43+
MouseEventKind::ScrollDown => $this->scrollDown(),
44+
default => null,
45+
};
46+
}
47+
}
48+
}

src/Console/UI/Components/Search.php

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Soap\WsdlReader\Console\UI\Components;
4+
5+
use PhpTui\Tui\Extension\Core\Widget\BlockWidget;
6+
use PhpTui\Tui\Extension\Core\Widget\ParagraphWidget;
7+
use PhpTui\Tui\Text\Line;
8+
use PhpTui\Tui\Text\Title;
9+
use PhpTui\Tui\Widget\Borders;
10+
use PhpTui\Tui\Widget\Widget;
11+
12+
final class Search
13+
{
14+
public static function create(SearchState $state): Widget
15+
{
16+
return BlockWidget::default()
17+
->titles(Title::fromString('Search'))
18+
->borders(Borders::ALL)
19+
->widget(
20+
ParagraphWidget::fromLines(Line::fromString($state->query)),
21+
);
22+
}
23+
}

0 commit comments

Comments
 (0)