Skip to content

Commit acc6f1c

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

35 files changed

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

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, JSON_UNESCAPED_SLASHES),
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 + JSON_UNESCAPED_SLASHES) : $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)