diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..18ecd19
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,13 @@
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
+indent_style = space
+indent_size = 4
+
+[*.{yml,yaml,json,xml}]
+indent_size = 2[*.md]
+trim_trailing_whitespace = false
\ No newline at end of file
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..372d125
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,22 @@
+# Set default behavior
+* text=auto
+
+# Enforce LF endings for scripts and configs (Crucial for CLI tools running on Linux/Docker)
+*.php text eol=lf
+*.json text eol=lf
+*.yml text eol=lf
+*.xml text eol=lf
+*.sh text eol=lf
+
+# ── EXPORT IGNORE (Keeps production installations tiny) ───────
+/tests/ export-ignore
+/.github/ export-ignore
+/.idea/ export-ignore
+/.vscode/ export-ignore
+/phpunit.xml.dist export-ignore
+/phpstan.neon export-ignore
+/rector.php export-ignore
+/.php-cs-fixer.php export-ignore
+/infection.json5 export-ignore
+/.gitignore export-ignore
+/.editorconfig export-ignore
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index c6597e4..c36c1be 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,25 +1,53 @@
-# CakePHP 3
+# ── COMPOSER ──────────────────────────────────────────────────
+/vendor/
+#(Uncomment if you want to ignore lock file for the library)
+composer.lock
-/vendor/*
-/config/app.php
-/tmp/cache/models/*
-!/tmp/cache/models/empty
-/tmp/cache/persistent/*
-!/tmp/cache/persistent/empty
-/tmp/cache/views/*
-!/tmp/cache/views/empty
-/tmp/sessions/*
-!/tmp/sessions/empty
-/tmp/tests/*
-!/tmp/tests/empty
+# ── TESTING & COVERAGE ────────────────────────────────────────
+/.phpunit.cache/
+.phpunit.result.cache
+/coverage/
+/coverage-html/
+coverage.xml
+clover.xml
+phpunit.xml
+!phpunit.xml.dist
+infection.log
-/logs/*
-!/logs/empty
+# ── STATIC ANALYSIS & REFACTORING CACHES ──────────────────────
+.php-cs-fixer.cache
+/.phpstan.cache/
+/.rector/
+phpstan-results.json
-# CakePHP 2
+# ── PROFILING & DEBUGGING (Xdebug) ────────────────────────────
+cachegrind.out.*
+xdebug.log
+error_log
+php_errors.log
-/app/tmp/*
-/app/Config/core.php
-/app/Config/database.php
-/vendors/*
+# ── BUILD ARTIFACTS (If you compile the CLI) ──────────────────
+/build/
+/dist/
+*.phar
+
+# ── ENVIRONMENT & DOCKER ──────────────────────────────────────
+.env
+.env.*.local
+docker-compose.override.yml
+docker-compose.local.yml
+
+# ── OS & IDEs ─────────────────────────────────────────────────
+.DS_Store
+.AppleDouble
+.LSOverride
+Icon
+._*
+Thumbs.db
+ehthumbs.db
+/.idea/
+/.vscode/
+*.swp
+*.swo
+*~
\ No newline at end of file
diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php
new file mode 100644
index 0000000..c141715
--- /dev/null
+++ b/.php-cs-fixer.dist.php
@@ -0,0 +1,29 @@
+setRiskyAllowed(true)
+ ->setRules([
+ '@auto' => true,
+ '@auto:risky' => true
+ ])
+ // 💡 by default, Fixer looks for `*.php` files excluding `./vendor/` - here, you can groom this config
+ ->setFinder(
+ (new Finder())
+ // 💡 root folder to check
+ ->in(__DIR__)
+ // 💡 additional files, eg bin entry file
+ // ->append([__DIR__.'/bin-entry-file'])
+ // 💡 folders to exclude, if any
+ // ->exclude([/* ... */])
+ // 💡 path patterns to exclude, if any
+ // ->notPath([/* ... */])
+ // 💡 extra configs
+ // ->ignoreDotFiles(false) // true by default in v3, false in v4 or future mode
+ // ->ignoreVCS(true) // true by default
+ )
+;
diff --git a/composer.json b/composer.json
index 54ee7bc..4f1e0a6 100644
--- a/composer.json
+++ b/composer.json
@@ -3,21 +3,78 @@
"description": "Interactive CLI component runtime for PHP microservice and hexagonal architectures.",
"type": "library",
"license": "MIT",
+ "keywords":[
+ "cli",
+ "hexagonal-architecture",
+ "microservices",
+ "io",
+ "terminal"
+ ],
+ "authors":[
+ {
+ "name": "Alfacode Team",
+ "email": "shamavurasheed@gmail.com",
+ "role": "Developer"
+ }
+ ],
"autoload": {
"psr-4": {
"AlfacodeTeam\\PhpIoCli\\": "src/"
}
},
+ "autoload-dev": {
+ "psr-4": {
+ "AlfacodeTeam\\PhpIoCli\\Tests\\": "tests/"
+ }
+ },
"require": {
"php": "^8.2",
"psr/log": "^3.0"
},
"require-dev": {
+ "friendsofphp/php-cs-fixer": "^3.50",
+ "infection/infection": "^0.29",
+ "mockery/mockery": "^1.6",
"phpstan/phpstan": "^1.10",
+ "phpstan/phpstan-mockery": "^1.1",
+ "phpstan/phpstan-phpunit": "^1.3",
+ "phpunit/phpunit": "^10.5 || ^11.0",
+ "rector/rector": "^1.0",
"symfony/console": "^6.0 || ^7.0"
},
"minimum-stability": "dev",
"prefer-stable": true,
+ "config": {
+ "sort-packages": true,
+ "allow-plugins": {
+ "infection/extension-installer": true,
+ "phpstan/extension-installer": true
+ }
+ },
+ "scripts": {
+ "test": "phpunit",
+ "test:coverage": "phpunit --coverage-html coverage",
+ "format": "php-cs-fixer fix --allow-risky=yes",
+ "lint": "php-cs-fixer fix --dry-run --allow-risky=yes",
+ "analyse": "phpstan analyse --memory-limit=256M",
+ "refactor": "rector process",
+ "mutation": "infection --threads=max",
+ "check":[
+ "@lint",
+ "@analyse",
+ "@test"
+ ]
+ },
+ "scripts-descriptions": {
+ "test": "Run unit tests",
+ "test:coverage": "Run unit tests with HTML coverage report",
+ "format": "Automatically format PHP code using PHP-CS-Fixer",
+ "lint": "Check coding standards without modifying files",
+ "analyse": "Run static analysis with PHPStan",
+ "refactor": "Run Rector to automatically upgrade code and apply design patterns",
+ "mutation": "Run mutation testing with Infection to verify test suite quality",
+ "check": "Run all checks (linting, static analysis, and tests)"
+ },
"extra": {
"_comment": "── php-io-cli: command auto-discovery ──────────────────────────────────────",
"_comment2": "Applications that depend on this library should add their own 'extra.php-io-cli'",
@@ -25,7 +82,7 @@
"_example": {
"extra": {
"php-io-cli": {
- "commands": [
+ "commands":[
"App\\Commands\\ServeCommand",
"App\\Commands\\MakeModelCommand",
"App\\Commands\\MigrateCommand"
diff --git a/examples/01-inputs.php b/examples/01-inputs.php
index a618bb1..1e795a2 100644
--- a/examples/01-inputs.php
+++ b/examples/01-inputs.php
@@ -1,5 +1,6 @@
#!/usr/bin/env php
placeholder('e.g. Alice')
->default('World')
- ->validate(function (string $value): ?string {
- return mb_strlen($value) >= 2 ? null : 'Name must be at least 2 characters.';
- })
+ ->validate(fn(string $value): ?string => mb_strlen($value) >= 2 ? null : 'Name must be at least 2 characters.')
->run();
Colors::line(" → Name: {$name}", Colors::GREEN);
@@ -52,7 +51,7 @@
->showStrength()
->run();
-Colors::line(" → Password length: " . mb_strlen((string)$secret) . " chars", Colors::GREEN);
+Colors::line(" → Password length: " . mb_strlen((string) $secret) . " chars", Colors::GREEN);
// ── 4. Confirm ────────────────────────────────────────────────────
diff --git a/examples/02-display.php b/examples/02-display.php
index 4bc0f9e..b04bd5f 100644
--- a/examples/02-display.php
+++ b/examples/02-display.php
@@ -1,5 +1,6 @@
#!/usr/bin/env php
headers(['Service', 'Status', 'Latency', 'Requests'])
->rows([
- ['api-gateway', Colors::wrap('healthy', Colors::GREEN), '12 ms', '15,204'],
+ ['api-gateway', Colors::wrap('healthy', Colors::GREEN), '12 ms', '15,204'],
['auth-service', Colors::wrap('degraded', Colors::YELLOW), '340 ms', '3,891'],
- ['payment-worker', Colors::wrap('down', Colors::RED), '—', '0'],
- ['cache-service', Colors::wrap('healthy', Colors::GREEN), '2 ms', '52,001'],
+ ['payment-worker', Colors::wrap('down', Colors::RED), '—', '0'],
+ ['cache-service', Colors::wrap('healthy', Colors::GREEN), '2 ms', '52,001'],
])
->align([3 => 'right'])
->render();
diff --git a/examples/03-application.php b/examples/03-application.php
index 542e525..ea2e58d 100644
--- a/examples/03-application.php
+++ b/examples/03-application.php
@@ -1,5 +1,6 @@
#!/usr/bin/env php
description = 'Deploy the application to an environment';
$this->addArgument('environment', 'Target environment', required: true);
- $this->addOption('tag', 't', 'Git tag to deploy', acceptsValue: true, default: 'latest');
+ $this->addOption('tag', 't', 'Git tag to deploy', acceptsValue: true, default: 'latest');
$this->addOption('dry-run', 'd', 'Simulate without side-effects');
- $this->addOption('force', 'f', 'Skip confirmation prompt');
+ $this->addOption('force', 'f', 'Skip confirmation prompt');
}
protected function handle(): int
@@ -107,7 +108,7 @@ protected function configure(): void
$this->description = 'Run pending database migrations';
$this->addOption('rollback', 'r', 'Rollback the last batch');
- $this->addOption('steps', 's', 'Number of steps to roll back', acceptsValue: true, default: '1');
+ $this->addOption('steps', 's', 'Number of steps to roll back', acceptsValue: true, default: '1');
}
protected function handle(): int
@@ -190,7 +191,7 @@ protected function handle(): int
$this->table()
->headers(['File', 'Status'])
->rows(array_map(
- fn (string $f) => ["src/{$name}/{$f}.php", Colors::wrap('created', Colors::GREEN)],
+ fn(string $f) => ["src/{$name}/{$f}.php", Colors::wrap('created', Colors::GREEN)],
$features
))
->render();
diff --git a/examples/04-shell.php b/examples/04-shell.php
index 723dcf1..c32ed76 100644
--- a/examples/04-shell.php
+++ b/examples/04-shell.php
@@ -1,5 +1,6 @@
#!/usr/bin/env php
+
+
+
+ tests
+
+
+
+
+ src
+
+
+
\ No newline at end of file
diff --git a/rector.php b/rector.php
new file mode 100644
index 0000000..e0578d1
--- /dev/null
+++ b/rector.php
@@ -0,0 +1,15 @@
+withPaths([
+ __DIR__ . '/examples',
+ __DIR__ . '/src',
+ __DIR__ . '/tests',
+ ])
+ // uncomment to reach your current PHP version
+ // ->withPhpSets()
+ ->withTypeCoverageLevel(0);
diff --git a/src/AbstractCommand.php b/src/AbstractCommand.php
index 8dc9e59..6eb99fa 100644
--- a/src/AbstractCommand.php
+++ b/src/AbstractCommand.php
@@ -1,4 +1,5 @@
io = $io;
- $this->rawTokens = $tokens;
$this->parseTokens($tokens);
- // Validate required arguments
+ // FIX 2: The old condition was:
+ // !isset($this->arguments[$argName]) || $this->arguments[$argName] === null
+ // isset() already returns false for null-valued keys, so after !isset() is false
+ // (i.e. the key IS set and IS NOT null), the === null comparison is always false.
+ // PHPStan level 8 correctly flags it. Fix: use only !isset(), which covers both
+ // "key absent" and "key set to null".
foreach ($this->argumentDefs as $argName => $def) {
- if ($def['required'] && (!isset($this->arguments[$argName]) || $this->arguments[$argName] === null)) {
+ if ($def['required'] && !isset($this->arguments[$argName])) {
$this->error("Missing required argument: <{$argName}>");
return self::INVALID;
}
@@ -75,6 +83,11 @@ final public function execute(array $tokens, IOInterface $io): int
try {
return $this->handle();
} catch (Throwable $e) {
+ // If catch-exceptions mode is enabled (via marker), rethrow
+ if ($this->rethrowExceptions) {
+ throw $e;
+ }
+
$this->io->error("Command Error: " . $e->getMessage());
if ($io->isDebug()) {
$this->io->write(Colors::muted($e->getTraceAsString()));
@@ -83,6 +96,17 @@ final public function execute(array $tokens, IOInterface $io): int
}
}
+ /**
+ * Set whether exceptions should be rethrown instead of caught.
+ * Called by CLIApplication when catchExceptions(false).
+ *
+ * @internal For use by CLIApplication only.
+ */
+ final public function setRethrowExceptions(bool $rethrow): void
+ {
+ $this->rethrowExceptions = $rethrow;
+ }
+
/* =========================================================
Registration
========================================================= */
@@ -112,10 +136,12 @@ protected function addOption(string $long, string $short = '', string $descripti
private function parseTokens(array $tokens): void
{
// Set Defaults
- foreach ($this->argumentDefs as $name => $def)
+ foreach ($this->argumentDefs as $name => $def) {
$this->arguments[$name] = $def['default'];
- foreach ($this->optionDefs as $key => $def)
+ }
+ foreach ($this->optionDefs as $key => $def) {
$this->options[$key] = $def['default'];
+ }
$positional = [];
$count = count($tokens);
@@ -131,7 +157,6 @@ private function parseTokens(array $tokens): void
$this->options[$key] = $value;
} else {
$this->options[$bare] = true;
- // Check if next token is a value
if (($this->optionDefs[$bare]['acceptsValue'] ?? false) && isset($tokens[$i + 1]) && !str_starts_with($tokens[$i + 1], '-')) {
$this->options[$bare] = $tokens[++$i];
}
@@ -162,8 +187,9 @@ private function parseTokens(array $tokens): void
$argNames = array_keys($this->argumentDefs);
foreach ($positional as $idx => $value) {
- if (isset($argNames[$idx]))
+ if (isset($argNames[$idx])) {
$this->arguments[$argNames[$idx]] = $value;
+ }
}
}
@@ -175,18 +201,17 @@ protected function option(string $name, mixed $default = null): mixed
{
return $this->options[ltrim($name, '-')] ?? $default;
}
- // protected function hasOption(string $name): bool { return (bool)$this->option($name); }
protected function argument(string $name, mixed $default = null): mixed
{
return $this->arguments[$name] ?? $default;
}
+
protected function hasOption(string $name): bool
{
return (bool) ($this->options[ltrim($name, '-')] ?? false);
}
-
protected function info(string $message): void
{
$this->io->write(Colors::info($message));
@@ -220,9 +245,10 @@ protected function section(string $title): void
$this->io->write(Colors::wrap($title, [Colors::BOLD, Colors::CYAN]));
$this->io->write(Colors::muted(str_repeat('─', mb_strlen(Colors::strip($title)))));
}
+
/* =========================================================
- Alert Components (Restored)
- ========================================================= */
+ Alert Components
+ ========================================================= */
protected function alertSuccess(string $title, string|array $body = []): void
{
@@ -244,7 +270,6 @@ protected function alertInfo(string $title, string|array $body = []): void
Alert::info($title, $body);
}
-
/* =========================================================
Component Factory Methods
========================================================= */
@@ -277,7 +302,6 @@ protected function spinner(string $label, string $style = 'dots'): SpinnerCompon
return new SpinnerComponent($label, $style);
}
-
/* =========================================================
Help Generation
========================================================= */
@@ -315,4 +339,8 @@ final public function getDescription(): string
{
return $this->description;
}
-}
\ No newline at end of file
+ final public function isHidden(): bool
+ {
+ return $this->hidden;
+ }
+}
diff --git a/src/AbstractPrompt.php b/src/AbstractPrompt.php
index 6e23e3a..20d8fb8 100644
--- a/src/AbstractPrompt.php
+++ b/src/AbstractPrompt.php
@@ -1,4 +1,5 @@
write($messages, $newline, $verbosity);
}
- public function writeErrorRaw($messages, bool $newline = true, int $verbosity = self::NORMAL): void
+ public function writeErrorRaw(string|array $messages, bool $newline = true, int $verbosity = self::NORMAL): void
{
$this->writeError($messages, $newline, $verbosity);
}
@@ -30,28 +32,44 @@ public function writeErrorRaw($messages, bool $newline = true, int $verbosity =
========================================================= */
public function emergency(string|Stringable $message, array $context = []): void
- { $this->log(LogLevel::EMERGENCY, $message, $context); }
+ {
+ $this->log(LogLevel::EMERGENCY, $message, $context);
+ }
public function alert(string|Stringable $message, array $context = []): void
- { $this->log(LogLevel::ALERT, $message, $context); }
+ {
+ $this->log(LogLevel::ALERT, $message, $context);
+ }
public function critical(string|Stringable $message, array $context = []): void
- { $this->log(LogLevel::CRITICAL, $message, $context); }
+ {
+ $this->log(LogLevel::CRITICAL, $message, $context);
+ }
public function error(string|Stringable $message, array $context = []): void
- { $this->log(LogLevel::ERROR, $message, $context); }
+ {
+ $this->log(LogLevel::ERROR, $message, $context);
+ }
public function warning(string|Stringable $message, array $context = []): void
- { $this->log(LogLevel::WARNING, $message, $context); }
+ {
+ $this->log(LogLevel::WARNING, $message, $context);
+ }
public function notice(string|Stringable $message, array $context = []): void
- { $this->log(LogLevel::NOTICE, $message, $context); }
+ {
+ $this->log(LogLevel::NOTICE, $message, $context);
+ }
public function info(string|Stringable $message, array $context = []): void
- { $this->log(LogLevel::INFO, $message, $context); }
+ {
+ $this->log(LogLevel::INFO, $message, $context);
+ }
public function debug(string|Stringable $message, array $context = []): void
- { $this->log(LogLevel::DEBUG, $message, $context); }
+ {
+ $this->log(LogLevel::DEBUG, $message, $context);
+ }
/**
* Core logging logic with ANSI theming and safe JSON context serialisation.
@@ -64,8 +82,7 @@ public function log($level, $message, array $context = []): void
$output = (string) $message;
if ($context !== []) {
- // Silencer is now properly imported — this call will resolve correctly.
- $json = Silencer::call(static fn () => json_encode(
+ $json = Silencer::call(static fn() => json_encode(
$context,
JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_IGNORE
));
@@ -105,4 +122,4 @@ public function log($level, $message, array $context = []): void
$this->write($formatted, true, $targetVerbosity);
}
-}
\ No newline at end of file
+}
diff --git a/src/BufferIO.php b/src/BufferIO.php
index bfbe21c..e90c62d 100644
--- a/src/BufferIO.php
+++ b/src/BufferIO.php
@@ -1,4 +1,5 @@
-input instanceof StreamableInputInterface) {
throw new \RuntimeException('Setting the user inputs requires at least the version 3.2 of the symfony/console component.');
}
@@ -95,11 +96,11 @@ private function createStream(array $inputs)
}
foreach ($inputs as $input) {
- fwrite($stream, $input.PHP_EOL);
+ fwrite($stream, $input . PHP_EOL);
}
rewind($stream);
return $stream;
}
-}
\ No newline at end of file
+}
diff --git a/src/CLIApplication.php b/src/CLIApplication.php
index ea8079e..2b2aa43 100644
--- a/src/CLIApplication.php
+++ b/src/CLIApplication.php
@@ -1,4 +1,5 @@
commands;
if (!$includeHidden) {
- $cmds = array_filter($cmds, fn ($c) => !$c->isHidden());
+ $cmds = array_filter($cmds, fn($c) => !$c->isHidden());
}
ksort($cmds);
@@ -256,7 +257,9 @@ private function locateComposerJson(): string
}
$parent = dirname($dir);
- if ($parent === $dir) break; // filesystem root
+ if ($parent === $dir) {
+ break;
+ } // filesystem root
$dir = $parent;
}
@@ -276,7 +279,7 @@ private function locateComposerJson(): string
*/
public function run(?array $argv = null): int
{
- $argv = $argv ?? array_slice($_SERVER['argv'] ?? [], 1);
+ $argv ??= array_slice($_SERVER['argv'] ?? [], 1);
$token = $argv[0] ?? '';
$rest = array_slice($argv, 1);
@@ -320,12 +323,14 @@ private function dispatch(string $token, array $rest): int
{
// --help / -h attached to a known command
if (
- $token !== '' &&
- $this->has($token) &&
- (in_array('--help', $rest, true) || in_array('-h', $rest, true))
+ $token !== ''
+ && $this->has($token)
+ && (in_array('--help', $rest, true) || in_array('-h', $rest, true))
) {
- $this->commands[$token]->execute([], $this->io());
- $this->commands[$token]->printHelp();
+ $cmd = $this->commands[$token];
+ $cmd->setRethrowExceptions(!$this->catchExceptions);
+ $cmd->execute([], $this->io());
+ $cmd->printHelp();
return AbstractCommand::SUCCESS;
}
@@ -346,7 +351,9 @@ private function dispatch(string $token, array $rest): int
// Exact match
if ($this->has($token)) {
- return $this->commands[$token]->execute($rest, $this->io());
+ $cmd = $this->commands[$token];
+ $cmd->setRethrowExceptions(!$this->catchExceptions);
+ return $cmd->execute($rest, $this->io());
}
// ── Not found — fuzzy suggestions ─────────────────────
@@ -360,7 +367,9 @@ private function dispatch(string $token, array $rest): int
$this->io()->write(Colors::muted(' Did you mean one of these?') . PHP_EOL);
$pick = (new CustomSelect('Run instead?', $suggestions))->run();
if (is_string($pick) && $this->has($pick)) {
- return $this->commands[$pick]->execute($rest, $this->io());
+ $cmd = $this->commands[$pick];
+ $cmd->setRethrowExceptions(!$this->catchExceptions);
+ return $cmd->execute($rest, $this->io());
}
} else {
$this->io()->writeError('');
@@ -398,6 +407,7 @@ private function cmdHelp(string $commandName): int
}
$cmd = $this->commands[$commandName];
+ $cmd->setRethrowExceptions(!$this->catchExceptions);
$cmd->execute([], $this->io());
$cmd->printHelp();
return AbstractCommand::SUCCESS;
@@ -432,7 +442,7 @@ private function cmdList(): int
}
// Align descriptions by padding command names to same width
- $maxLen = max(array_map(fn ($n) => mb_strlen($n), array_keys($cmds)));
+ $maxLen = max(array_map(fn($n) => mb_strlen($n), array_keys($cmds)));
foreach ($cmds as $name => $cmd) {
$this->io()->write(sprintf(
@@ -494,4 +504,4 @@ private function isTty(): bool
{
return function_exists('posix_isatty') && @posix_isatty(STDIN);
}
-}
\ No newline at end of file
+}
diff --git a/src/Components/Alert.php b/src/Components/Alert.php
index f9408b9..98221aa 100644
--- a/src/Components/Alert.php
+++ b/src/Components/Alert.php
@@ -1,4 +1,5 @@
input->fallback(function ($s, $key) {
+ $this->input->fallback(function ($s, $key): void {
if (Key::isPrintable($key)) {
- $cur = (int)$s->cursor;
- $val = (string)$s->value;
-
+ $cur = (int) $s->cursor;
+ $val = (string) $s->value;
+
$s->value = mb_substr($val, 0, $cur) . $key . mb_substr($val, $cur);
$s->cursor = $cur + 1;
$s->focused = 0; // Reset focus on type
@@ -57,45 +58,47 @@ protected function setup(): void
});
// 2. Navigation & Deletion
- $this->input->bind('BACKSPACE', function ($s) {
- $cur = (int)$s->cursor;
- if ($cur === 0) return;
-
- $val = (string)$s->value;
+ $this->input->bind('BACKSPACE', function ($s): void {
+ $cur = (int) $s->cursor;
+ if ($cur === 0) {
+ return;
+ }
+
+ $val = (string) $s->value;
$s->value = mb_substr($val, 0, $cur - 1) . mb_substr($val, $cur);
$s->cursor = $cur - 1;
$s->focused = 0;
});
- $this->input->bind('LEFT', fn($s) => $s->decrement('cursor'));
- $this->input->bind('RIGHT', fn($s) => $s->increment('cursor', mb_strlen((string)$s->value)));
+ $this->input->bind('LEFT', fn($s) => $s->decrement('cursor'));
+ $this->input->bind('RIGHT', fn($s) => $s->increment('cursor', mb_strlen((string) $s->value)));
- $this->input->bind('UP', function ($s) {
+ $this->input->bind('UP', function ($s): void {
$s->decrement('focused');
});
- $this->input->bind('DOWN', function ($s) {
+ $this->input->bind('DOWN', function ($s): void {
$count = count($this->getFiltered());
$s->increment('focused', $count > 0 ? $count - 1 : 0);
});
// 3. Selection Logic
- $this->input->bind('TAB', function ($s) {
+ $this->input->bind('TAB', function ($s): void {
$filtered = $this->getFiltered();
if (!empty($filtered)) {
- $selection = $filtered[(int)$s->focused] ?? $filtered[0];
+ $selection = $filtered[(int) $s->focused] ?? $filtered[0];
$s->value = $selection;
$s->cursor = mb_strlen($selection);
}
});
- $this->input->bind('ENTER', function ($s) {
+ $this->input->bind('ENTER', function ($s): void {
$filtered = $this->getFiltered();
- $val = (string)$s->value;
+ $val = (string) $s->value;
// If a suggestion is highlighted and different from current text, "fill" it first
if (!empty($filtered) && $val !== '') {
- $highlighted = $filtered[(int)$s->focused] ?? null;
+ $highlighted = $filtered[(int) $s->focused] ?? null;
if ($highlighted && $highlighted !== $val) {
$s->value = $highlighted;
$s->cursor = mb_strlen($highlighted);
@@ -114,21 +117,21 @@ public function render(): void
Terminal::moveCursorUp($this->lastLines);
}
- $val = (string)$this->state->value;
- $cur = (int)$this->state->cursor;
- $focused = (int)$this->state->focused;
+ $val = (string) $this->state->value;
+ $cur = (int) $this->state->cursor;
+ $focused = (int) $this->state->focused;
$filtered = $this->getFiltered();
$lines = [];
// HEADER
$lines[] = Colors::wrap('? ', Colors::CYAN) . Colors::wrap($this->question, Colors::BOLD);
- if (!(bool)$this->state->done) {
+ if (!(bool) $this->state->done) {
// INPUT LINE WITH VIRTUAL CARET
$before = mb_substr($val, 0, $cur);
$at = mb_substr($val, $cur, 1);
$after = mb_substr($val, $cur + 1);
-
+
// Caret styling (Reverse Video)
$caretChar = ($at === '') ? ' ' : $at;
$caret = Colors::wrap($caretChar, ["\033[7m", Colors::YELLOW]);
@@ -161,7 +164,7 @@ public function render(): void
private function renderDropdown(array $filtered, int $focused): array
{
$visible = array_slice($filtered, 0, $this->maxSuggestions);
-
+
// Calculate dynamic width based on content
$width = $this->minDropdownWidth;
foreach ($visible as $item) {
@@ -175,10 +178,10 @@ private function renderDropdown(array $filtered, int $focused): array
foreach ($visible as $i => $item) {
$active = $i === $focused;
-
+
$icon = $active ? Colors::wrap('› ', Colors::GREEN) : ' ';
$text = $active ? Colors::wrap($item, [Colors::YELLOW, Colors::BOLD]) : Colors::muted($item);
-
+
// Padded line construction
$contentLen = mb_strlen(Colors::strip($item));
$padding = str_repeat(' ', max(0, $width - $contentLen - 4));
@@ -203,6 +206,6 @@ public function resolve(): mixed
private function getFiltered(): array
{
- return Fuzzy::filter($this->suggestions, (string)$this->state->value);
+ return Fuzzy::filter($this->suggestions, (string) $this->state->value);
}
-}
\ No newline at end of file
+}
diff --git a/src/Components/Component.php b/src/Components/Component.php
index 80715a5..7b8880e 100644
--- a/src/Components/Component.php
+++ b/src/Components/Component.php
@@ -1,4 +1,5 @@
run();
*/
final class Confirm extends Component
@@ -34,17 +35,19 @@ protected function setup(): void
public function render(): void
{
- if ($this->lastLines > 0) Terminal::moveCursorUp($this->lastLines);
-
+ if ($this->lastLines > 0) {
+ Terminal::moveCursorUp($this->lastLines);
+ }
+
$active = $this->state->confirmed;
-
+
$yes = $active ? Colors::wrap(' Yes ', [Colors::BG_CYAN, Colors::BLACK]) : Colors::wrap(' Yes ', Colors::GRAY);
$no = !$active ? Colors::wrap(' No ', [Colors::BG_CYAN, Colors::BLACK]) : Colors::wrap(' No ', Colors::GRAY);
$lines = [
Colors::wrap('? ', Colors::CYAN) . Colors::wrap($this->question, Colors::BOLD),
" {$yes} {$no}",
- Colors::muted(' ← → to toggle • Enter to confirm')
+ Colors::muted(' ← → to toggle • Enter to confirm'),
];
foreach ($lines as $line) {
@@ -56,6 +59,6 @@ public function render(): void
public function resolve(): bool
{
- return (bool)$this->state->confirmed;
+ return (bool) $this->state->confirmed;
}
-}
\ No newline at end of file
+}
diff --git a/src/Components/DatePicker.php b/src/Components/DatePicker.php
index 073bad0..b3998a1 100644
--- a/src/Components/DatePicker.php
+++ b/src/Components/DatePicker.php
@@ -1,4 +1,5 @@
state->batch([
- 'year' => (int)$now->format('Y'),
- 'month' => (int)$now->format('n'),
- 'day' => (int)$now->format('j'),
+ 'year' => (int) $now->format('Y'),
+ 'month' => (int) $now->format('n'),
+ 'day' => (int) $now->format('j'),
'done' => false,
]);
// 1. Precise Day/Week Navigation
- $this->input->bind('LEFT', fn($s) => $this->shiftDate($s, '-1 day'));
+ $this->input->bind('LEFT', fn($s) => $this->shiftDate($s, '-1 day'));
$this->input->bind('RIGHT', fn($s) => $this->shiftDate($s, '+1 day'));
- $this->input->bind('UP', fn($s) => $this->shiftDate($s, '-7 days'));
- $this->input->bind('DOWN', fn($s) => $this->shiftDate($s, '+7 days'));
+ $this->input->bind('UP', fn($s) => $this->shiftDate($s, '-7 days'));
+ $this->input->bind('DOWN', fn($s) => $this->shiftDate($s, '+7 days'));
// 2. Month Navigation (Mapped to [ and ] or PageUp/Down if your Terminal detects them)
- $this->input->bind(['[', 'PAGE_UP'], fn($s) => $this->shiftMonth($s, -1));
+ $this->input->bind(['[', 'PAGE_UP'], fn($s) => $this->shiftMonth($s, -1));
$this->input->bind([']', 'PAGE_DOWN'], fn($s) => $this->shiftMonth($s, 1));
// 3. Shortcuts
- $this->input->bind('t', function($s) {
+ $this->input->bind('t', function ($s): void {
$now = new DateTimeImmutable();
$s->batch([
- 'year' => (int)$now->format('Y'),
- 'month' => (int)$now->format('n'),
- 'day' => (int)$now->format('j'),
+ 'year' => (int) $now->format('Y'),
+ 'month' => (int) $now->format('n'),
+ 'day' => (int) $now->format('j'),
]);
});
- $this->input->bind('ENTER', function ($s) {
+ $this->input->bind('ENTER', function ($s): void {
$s->done = true;
$this->stop();
});
@@ -65,9 +66,9 @@ private function shiftDate(mixed $s, string $modify): void
{
$dt = $this->getSelectedDate()->modify($modify);
$s->batch([
- 'year' => (int)$dt->format('Y'),
- 'month' => (int)$dt->format('n'),
- 'day' => (int)$dt->format('j'),
+ 'year' => (int) $dt->format('Y'),
+ 'month' => (int) $dt->format('n'),
+ 'day' => (int) $dt->format('j'),
]);
}
@@ -76,25 +77,25 @@ private function shiftMonth(mixed $s, int $delta): void
$dt = $this->getSelectedDate();
// Modify month first
$newDt = $dt->modify(($delta > 0 ? '+' : '') . $delta . ' months');
-
+
// Handle PHP "overflow" (Jan 31 + 1 month = March 3). Clamp to last day of month.
- if ((int)$newDt->format('n') !== ($dt->format('n') + $delta + 12) % 12 ?: 12) {
- $newDt = $newDt->modify('last day of last month');
+ if ((int) $newDt->format('n') !== ($dt->format('n') + $delta + 12) % 12 ?: 12) {
+ $newDt = $newDt->modify('last day of last month');
}
$s->batch([
- 'year' => (int)$newDt->format('Y'),
- 'month' => (int)$newDt->format('n'),
- 'day' => (int)$newDt->format('j'),
+ 'year' => (int) $newDt->format('Y'),
+ 'month' => (int) $newDt->format('n'),
+ 'day' => (int) $newDt->format('j'),
]);
}
private function getSelectedDate(): DateTimeImmutable
{
return new DateTimeImmutable(sprintf(
- '%04d-%02d-%02d',
- $this->state->year,
- $this->state->month,
+ '%04d-%02d-%02d',
+ $this->state->year,
+ $this->state->month,
$this->state->day
));
}
@@ -112,10 +113,10 @@ public function render(): void
Terminal::moveCursorUp($this->lastLines);
}
- $year = (int)$this->state->year;
- $month = (int)$this->state->month;
- $day = (int)$this->state->day;
- $done = (bool)$this->state->done;
+ $year = (int) $this->state->year;
+ $month = (int) $this->state->month;
+ $day = (int) $this->state->day;
+ $done = (bool) $this->state->done;
$lines = [];
$lines[] = Colors::wrap('? ', Colors::CYAN) . Colors::wrap($this->question, Colors::BOLD);
@@ -123,8 +124,8 @@ public function render(): void
if (!$done) {
$currentDt = $this->getSelectedDate();
$firstOfMonth = $currentDt->modify('first day of this month');
- $daysInMonth = (int)$currentDt->format('t');
- $startColumn = (int)$firstOfMonth->format('N') - 1; // 0 (Mon) to 6 (Sun)
+ $daysInMonth = (int) $currentDt->format('t');
+ $startColumn = (int) $firstOfMonth->format('N') - 1; // 0 (Mon) to 6 (Sun)
$lines[] = '';
$lines[] = ' ' . Colors::wrap($currentDt->format('F Y'), [Colors::BOLD, Colors::CYAN]);
@@ -136,9 +137,9 @@ public function render(): void
for ($d = 1; $d <= $daysInMonth; $d++) {
$isToday = $this->isToday($year, $month, $d);
$isSelected = ($d === $day);
-
- $label = str_pad((string)$d, 2, ' ', STR_PAD_LEFT);
-
+
+ $label = str_pad((string) $d, 2, ' ', STR_PAD_LEFT);
+
if ($isSelected) {
// Reverse video for the "Cursor"
$cell = Colors::wrap(" {$label}", ["\033[7m", Colors::CYAN, Colors::BOLD]);
diff --git a/src/Components/MultiSelect.php b/src/Components/MultiSelect.php
index 198e62f..5cf3051 100644
--- a/src/Components/MultiSelect.php
+++ b/src/Components/MultiSelect.php
@@ -1,4 +1,5 @@
run();
*/
final class MultiSelect extends Component
@@ -27,11 +28,11 @@ protected function setup(): void
$this->input->bind('UP', fn($s) => $s->decrement('index'));
$this->input->bind('DOWN', fn($s) => $s->increment('index', count($this->choices) - 1));
- $this->input->bind(' ', function($s) {
+ $this->input->bind(' ', function ($s): void {
$val = $this->choices[$s->index];
$s->toggle('selected', $val);
});
- $this->input->bind('ENTER', function($s) {
+ $this->input->bind('ENTER', function ($s): void {
$s->done = true;
$this->stop();
});
@@ -39,15 +40,17 @@ protected function setup(): void
public function render(): void
{
- if ($this->lastLines > 0) Terminal::moveCursorUp($this->lastLines);
-
+ if ($this->lastLines > 0) {
+ Terminal::moveCursorUp($this->lastLines);
+ }
+
$lines = [Colors::wrap('? ', Colors::CYAN) . Colors::wrap($this->question, Colors::BOLD)];
if (!$this->state->done) {
foreach ($this->choices as $i => $item) {
$active = $i === $this->state->index;
- $checked = in_array($item, (array)$this->state->selected, true);
-
+ $checked = in_array($item, (array) $this->state->selected, true);
+
$pointer = $active ? Colors::wrap('› ', Colors::CYAN) : ' ';
$box = $checked ? Colors::wrap('⬢ ', Colors::GREEN) : Colors::wrap('⬡ ', Colors::GRAY);
$label = $active ? Colors::wrap($item, Colors::YELLOW) : Colors::wrap($item, Colors::GRAY);
@@ -56,7 +59,7 @@ public function render(): void
}
$lines[] = Colors::muted(' ↑↓ move • Space toggle • Enter submit');
} else {
- $lines[] = Colors::wrap(' › ', Colors::GRAY) . Colors::wrap(implode(', ', (array)$this->state->selected), Colors::GREEN);
+ $lines[] = Colors::wrap(' › ', Colors::GRAY) . Colors::wrap(implode(', ', (array) $this->state->selected), Colors::GREEN);
}
foreach ($lines as $line) {
@@ -66,8 +69,8 @@ public function render(): void
$this->lastLines = count($lines);
}
- public function resolve(): array
+ public function resolve(): array
{
- return (array)$this->state->selected;
+ return (array) $this->state->selected;
}
-}
\ No newline at end of file
+}
diff --git a/src/Components/NumberInput.php b/src/Components/NumberInput.php
index a874bea..e70198e 100644
--- a/src/Components/NumberInput.php
+++ b/src/Components/NumberInput.php
@@ -1,4 +1,5 @@
min = $v; return $this; }
- public function max(float $v): self { $this->max = $v; return $this; }
- public function step(float $v): self { $this->step = $v; return $this; }
- public function default(float $v): self{ $this->default = $v; return $this; }
- public function integer(): self { $this->intOnly = true; return $this; }
+ public function min(float $v): self
+ {
+ $this->min = $v;
+ return $this;
+ }
+ public function max(float $v): self
+ {
+ $this->max = $v;
+ return $this;
+ }
+ public function step(float $v): self
+ {
+ $this->step = $v;
+ return $this;
+ }
+ public function default(float $v): self
+ {
+ $this->default = $v;
+ return $this;
+ }
+ public function integer(): self
+ {
+ $this->intOnly = true;
+ return $this;
+ }
/* =========================================================
LIFECYCLE
@@ -42,45 +63,51 @@ public function integer(): self { $this->intOnly = true; return $this; }
protected function setup(): void
{
$this->state->batch([
- 'raw' => $this->default !== null ? (string)$this->default : '',
+ 'raw' => $this->default !== null ? (string) $this->default : '',
'error' => null,
'done' => false,
]);
// Typing digits, minus, dot
- $this->input->fallback(function ($s, $key) {
- if (!Key::isPrintable($key)) return;
+ $this->input->fallback(function ($s, $key): void {
+ if (!Key::isPrintable($key)) {
+ return;
+ }
$allowed = $this->intOnly ? '0123456789-' : '0123456789.-';
if (mb_strpos($allowed, $key) !== false) {
- $s->raw = (string)$s->raw . $key;
+ $s->raw = (string) $s->raw . $key;
$s->error = null;
}
});
- $this->input->bind('BACKSPACE', function ($s) {
- $s->raw = mb_substr((string)$s->raw, 0, -1);
+ $this->input->bind('BACKSPACE', function ($s): void {
+ $s->raw = mb_substr((string) $s->raw, 0, -1);
$s->error = null;
});
// Arrow stepping
- $this->input->bind('UP', function ($s) {
- $current = (float)((string)$s->raw ?: '0');
+ $this->input->bind('UP', function ($s): void {
+ $current = (float) ((string) $s->raw ?: '0');
$new = $current + $this->step;
- if ($this->max !== null) $new = min($new, $this->max);
+ if ($this->max !== null) {
+ $new = min($new, $this->max);
+ }
$s->raw = $this->format($new);
});
- $this->input->bind('DOWN', function ($s) {
- $current = (float)((string)$s->raw ?: '0');
+ $this->input->bind('DOWN', function ($s): void {
+ $current = (float) ((string) $s->raw ?: '0');
$new = $current - $this->step;
- if ($this->min !== null) $new = max($new, $this->min);
+ if ($this->min !== null) {
+ $new = max($new, $this->min);
+ }
$s->raw = $this->format($new);
});
- $this->input->bind('ENTER', function ($s) {
- $raw = trim((string)$s->raw);
+ $this->input->bind('ENTER', function ($s): void {
+ $raw = trim((string) $s->raw);
if ($raw === '' && $this->default !== null) {
- $raw = (string)$this->default;
+ $raw = (string) $this->default;
}
if ($raw === '') {
@@ -93,7 +120,7 @@ protected function setup(): void
return;
}
- $val = (float)$raw;
+ $val = (float) $raw;
if ($this->min !== null && $val < $this->min) {
$s->error = "Minimum value is {$this->min}.";
@@ -113,7 +140,7 @@ protected function setup(): void
private function format(float $v): string
{
- return $this->intOnly ? (string)(int)$v : rtrim(rtrim(number_format($v, 10, '.', ''), '0'), '.');
+ return $this->intOnly ? (string) (int) $v : rtrim(rtrim(number_format($v, 10, '.', ''), '0'), '.');
}
/* =========================================================
@@ -128,9 +155,9 @@ public function render(): void
Terminal::hideCursor();
- $raw = (string)$this->state->raw;
+ $raw = (string) $this->state->raw;
$error = $this->state->error;
- $done = (bool)$this->state->done;
+ $done = (bool) $this->state->done;
$lines = [];
$mark = $done ? Colors::success('') : Colors::wrap('? ', Colors::CYAN);
@@ -178,7 +205,7 @@ public function destroy(): void
public function resolve(): mixed
{
- $v = (float)$this->state->raw;
- return $this->intOnly ? (int)$v : $v;
+ $v = (float) $this->state->raw;
+ return $this->intOnly ? (int) $v : $v;
}
-}
\ No newline at end of file
+}
diff --git a/src/Components/Password.php b/src/Components/Password.php
index f826722..68b9ee2 100644
--- a/src/Components/Password.php
+++ b/src/Components/Password.php
@@ -1,4 +1,5 @@
input->fallback(function ($state, $key) {
+ $this->input->fallback(function ($state, $key): void {
if (Key::isPrintable($key)) {
$state->value .= $key;
}
});
- $this->input->bind('BACKSPACE', function ($state) {
+ $this->input->bind('BACKSPACE', function ($state): void {
$state->value = mb_substr((string) $state->value, 0, -1);
});
// TAB toggles visibility
- $this->input->bind('TAB', function ($state) {
+ $this->input->bind('TAB', function ($state): void {
$state->visible = !(bool) $state->visible;
});
- $this->input->bind('ENTER', function ($state) {
+ $this->input->bind('ENTER', function ($state): void {
$state->done = true;
$this->stop();
});
@@ -97,7 +98,7 @@ public function render(): void
if ($this->strengthMeter && $len > 0) {
$lines[] = ' ' . $this->buildStrengthBar($value);
} else {
- $lines[] = '';
+ $lines[] = '';
}
// Help Hint
@@ -140,19 +141,29 @@ private function buildStrengthBar(string $password): string
{
$score = 0;
$len = mb_strlen($password);
-
- if ($len >= 8) $score++;
- if ($len >= 12) $score++;
- if (preg_match('/[A-Z]/', $password)) $score++;
- if (preg_match('/[0-9]/', $password)) $score++;
- if (preg_match('/[^A-Za-z0-9]/', $password)) $score++;
+
+ if ($len >= 8) {
+ $score++;
+ }
+ if ($len >= 12) {
+ $score++;
+ }
+ if (preg_match('/[A-Z]/', $password)) {
+ $score++;
+ }
+ if (preg_match('/[0-9]/', $password)) {
+ $score++;
+ }
+ if (preg_match('/[^A-Za-z0-9]/', $password)) {
+ $score++;
+ }
$labels = ['Very weak', 'Weak', 'Fair', 'Good', 'Strong'];
$colors = [Colors::RED, Colors::RED, Colors::YELLOW, Colors::GREEN, Colors::GREEN];
// Ensure we don't index out of bounds
$index = max(0, min($score - 1, 4));
-
+
$filled = str_repeat('━', $score);
$empty = str_repeat('━', 5 - $score);
$label = $labels[$index];
@@ -163,4 +174,4 @@ private function buildStrengthBar(string $password): string
. ' '
. Colors::wrap($label, $color);
}
-}
\ No newline at end of file
+}
diff --git a/src/Components/ProgressBar.php b/src/Components/ProgressBar.php
index 7afb32f..d1dc0bc 100644
--- a/src/Components/ProgressBar.php
+++ b/src/Components/ProgressBar.php
@@ -1,4 +1,5 @@
width = $w; return $this; }
+ public function width(int $w): self
+ {
+ $this->width = $w;
+ return $this;
+ }
/* =========================================================
CONTROL
@@ -66,7 +71,9 @@ public function advance(int $step = 1, string $status = ''): void
if ($this->total > 0) {
$this->current = min($this->current + $step, $this->total);
}
- if ($status !== '') $this->status = $status;
+ if ($status !== '') {
+ $this->status = $status;
+ }
$this->draw();
}
@@ -74,11 +81,13 @@ public function finish(string $message = ''): void
{
$this->finished = true;
$this->spinner->stop();
-
- if ($this->total > 0) $this->current = $this->total;
+
+ if ($this->total > 0) {
+ $this->current = $this->total;
+ }
$this->draw($message);
-
+
Terminal::showCursor();
echo PHP_EOL;
}
@@ -112,15 +121,15 @@ private function draw(string $finishMessage = ''): void
if ($this->total > 0) {
// DETERMINATE MODE (Bar + Spinner + Status)
$pct = $this->current / $this->total;
- $pctStr = str_pad((int)($pct * 100) . '%', 4, ' ', STR_PAD_LEFT);
+ $pctStr = str_pad((int) ($pct * 100) . '%', 4, ' ', STR_PAD_LEFT);
- $lines[] = "{$frame} " . Colors::wrap($this->label, Colors::BOLD)
+ $lines[] = "{$frame} " . Colors::wrap($this->label, Colors::BOLD)
. Colors::muted(sprintf(' (%d/%d)', $this->current, $this->total));
-
+
$lines[] = ' ' . $this->buildBar($pct) . ' ' . Colors::wrap($pctStr, Colors::YELLOW);
} else {
// INDETERMINATE MODE (Label + Spinner + Status)
- $lines[] = "{$frame} " . Colors::wrap($this->label, Colors::BOLD)
+ $lines[] = "{$frame} " . Colors::wrap($this->label, Colors::BOLD)
. Colors::muted(sprintf(' %.1fs', $elapsed));
}
@@ -155,4 +164,4 @@ private function buildBar(float $pct): string
return Colors::wrap(str_repeat($this->fillChar, $filledSize), $color)
. Colors::muted(str_repeat($this->emptyChar, $emptySize));
}
-}
\ No newline at end of file
+}
diff --git a/src/Components/Select.php b/src/Components/Select.php
index c11bfae..e8f0e7f 100644
--- a/src/Components/Select.php
+++ b/src/Components/Select.php
@@ -1,4 +1,5 @@
run();
*/
@@ -40,23 +41,23 @@ protected function setup(): void
]);
// Navigation
- $this->input->bind('UP', function ($state) {
+ $this->input->bind('UP', function ($state): void {
$state->decrement('index');
});
- $this->input->bind('DOWN', function ($state) {
+ $this->input->bind('DOWN', function ($state): void {
$count = count($this->getFiltered());
$state->increment('index', $count > 0 ? $count - 1 : 0);
});
// Search logic
- $this->input->bind('BACKSPACE', function ($state) {
- $state->search = mb_substr((string)$state->search, 0, -1);
+ $this->input->bind('BACKSPACE', function ($state): void {
+ $state->search = mb_substr((string) $state->search, 0, -1);
$state->index = 0;
});
// Selection
- $this->input->bind('ENTER', function ($state) {
+ $this->input->bind('ENTER', function ($state): void {
$filtered = $this->getFiltered();
if (empty($filtered)) {
return;
@@ -67,7 +68,7 @@ protected function setup(): void
});
// Typing fallback
- $this->input->fallback(function ($state, $key) {
+ $this->input->fallback(function ($state, $key): void {
if (Key::isPrintable($key)) {
$state->search .= $key;
$state->index = 0; // Reset index on new search
@@ -99,10 +100,10 @@ public function render(): void
if (!$done) {
// Search Bar Line
$searchLabel = Colors::wrap('› ', Colors::GRAY);
- $searchText = $search !== ''
- ? Colors::wrap($search, Colors::YELLOW) . Colors::wrap('▊', Colors::CYAN)
+ $searchText = $search !== ''
+ ? Colors::wrap($search, Colors::YELLOW) . Colors::wrap('▊', Colors::CYAN)
: Colors::wrap('Type to filter...', Colors::DIM);
-
+
$lines[] = " {$searchLabel}{$searchText}";
$lines[] = ""; // Spacer
@@ -114,7 +115,7 @@ public function render(): void
$windowSize = 8;
$total = count($filtered);
$start = (int) max(0, min($this->state->index - floor($windowSize / 2), $total - $windowSize));
-
+
foreach (array_slice($filtered, $start, $windowSize) as $i => $item) {
$realIndex = $start + $i;
$active = $realIndex === $this->state->index;
@@ -125,19 +126,19 @@ public function render(): void
$lines[] = Colors::wrap(" {$item}", Colors::GRAY);
}
}
-
+
// Scroll indicators for large lists
if ($total > $windowSize) {
$lines[] = Colors::muted(sprintf(" (Showing %d of %d)", $windowSize, $total));
}
}
-
+
$lines[] = "";
$lines[] = Colors::muted(" ↑↓ navigate • ENTER select • Type to filter");
} else {
// Collapse UI on finish
- $lines[] = Colors::wrap(' › ', Colors::GRAY) . Colors::wrap((string)$this->state->result, Colors::GREEN);
+ $lines[] = Colors::wrap(' › ', Colors::GRAY) . Colors::wrap((string) $this->state->result, Colors::GREEN);
}
// 3. Clear and print
@@ -166,6 +167,6 @@ public function resolve(): mixed
private function getFiltered(): array
{
- return Fuzzy::filter($this->choices, (string)$this->state->search);
+ return Fuzzy::filter($this->choices, (string) $this->state->search);
}
-}
\ No newline at end of file
+}
diff --git a/src/Components/SpinnerComponent.php b/src/Components/SpinnerComponent.php
index a234c17..e74030c 100644
--- a/src/Components/SpinnerComponent.php
+++ b/src/Components/SpinnerComponent.php
@@ -1,4 +1,5 @@
running) return;
+ if (!$this->running) {
+ return;
+ }
$this->draw($subLabel);
}
@@ -105,4 +108,4 @@ private function draw(string $subLabel = ''): void
$this->lastLines = count($lines);
}
-}
\ No newline at end of file
+}
diff --git a/src/Components/Table.php b/src/Components/Table.php
index 858151e..b99a5e7 100644
--- a/src/Components/Table.php
+++ b/src/Components/Table.php
@@ -1,4 +1,5 @@
headers = $headers; return $this; }
- public function rows(array $rows): self { $this->rows = $rows; return $this; }
- public function style(string $style): self { $this->style = $style; return $this; }
- public function striped(bool $enable = true): self { $this->striped = $enable; return $this; }
+ public function headers(array $headers): self
+ {
+ $this->headers = $headers;
+ return $this;
+ }
+ public function rows(array $rows): self
+ {
+ $this->rows = $rows;
+ return $this;
+ }
+ public function style(string $style): self
+ {
+ $this->style = $style;
+ return $this;
+ }
+ public function striped(bool $enable = true): self
+ {
+ $this->striped = $enable;
+ return $this;
+ }
/** @param array $alignments ['left', 'center', 'right'] */
- public function align(array $alignments): self { $this->alignments = $alignments; return $this; }
+ public function align(array $alignments): self
+ {
+ $this->alignments = $alignments;
+ return $this;
+ }
public function render(): void
{
@@ -89,7 +113,7 @@ private function getColumnCount(): int
}
/**
- * Fixes the "ANSI Headache":
+ * Fixes the "ANSI Headache":
* Uses Colors::strip() to calculate the VISUAL width of the content.
*/
private function computeWidths(int $count): array
@@ -100,7 +124,7 @@ private function computeWidths(int $count): array
foreach ($allData as $row) {
foreach ($row as $i => $cell) {
// We strip ANSI before measuring length
- $visualLength = mb_strlen(Colors::strip((string)$cell));
+ $visualLength = mb_strlen(Colors::strip((string) $cell));
$widths[$i] = max($widths[$i], $visualLength);
}
}
@@ -111,9 +135,9 @@ private function drawRow(array $cells, array $widths, string $sep, bool $isHeade
{
$parts = [];
foreach ($widths as $i => $width) {
- $rawContent = (string)($cells[$i] ?? '');
+ $rawContent = (string) ($cells[$i] ?? '');
$align = $this->alignments[$i] ?? 'left';
-
+
$padded = $this->applyPadding($rawContent, $width, $align);
// Styling logic
@@ -139,7 +163,7 @@ private function applyPadding(string $text, int $targetWidth, string $align): st
return match ($align) {
'right' => str_repeat(' ', $diff) . $text,
- 'center' => str_repeat(' ', (int)floor($diff / 2)) . $text . str_repeat(' ', (int)ceil($diff / 2)),
+ 'center' => str_repeat(' ', (int) floor($diff / 2)) . $text . str_repeat(' ', (int) ceil($diff / 2)),
default => $text . str_repeat(' ', $diff),
};
}
@@ -162,4 +186,4 @@ private function getBorders(): array
default => ['╔','╗','╚','╝','═','║','╦','╩','╠','╣','╬'],
};
}
-}
\ No newline at end of file
+}
diff --git a/src/Components/TextInput.php b/src/Components/TextInput.php
index 8d5162d..b308165 100644
--- a/src/Components/TextInput.php
+++ b/src/Components/TextInput.php
@@ -1,10 +1,12 @@
state->batch([
- 'value' => '',
- 'cursor' => 0,
- 'done' => false,
- 'error' => null,
+ 'value' => '',
+ 'cursor' => 0,
+ 'done' => false,
+ 'error' => null,
]);
// Key: Typing
- $this->input->fallback(function ($state, $key) {
+ $this->input->fallback(function (State|string $state, string $key): void {
if (Key::isPrintable($key)) {
- $cur = (int) $state->cursor;
+ $cur = (int) $state->cursor;
$value = (string) $state->value;
- $state->value = mb_substr($value, 0, $cur) . $key . mb_substr($value, $cur);
+ $state->value = mb_substr($value, 0, $cur) . $key . mb_substr($value, $cur);
$state->cursor = $cur + 1;
- $state->error = null;
+ $state->error = null;
}
});
// Key: Navigation
- $this->input->bind('LEFT', fn($s) => $s->decrement('cursor'));
- $this->input->bind('RIGHT', fn($s) => $s->increment('cursor', mb_strlen((string) $s->value)));
- $this->input->bind('HOME', fn($s) => $s->cursor = 0);
- $this->input->bind('END', fn($s) => $s->cursor = mb_strlen((string) $s->value));
+ $this->input->bind('LEFT', fn(State|string $s) => $s->decrement('cursor'));
+ $this->input->bind('RIGHT', fn(State|string $s) => $s->increment('cursor', mb_strlen((string) $s->value)));
+ $this->input->bind('HOME', function (State|string $s): void {
+ $s->cursor = 0;
+ });
+ $this->input->bind('END', function (State|string $s): void {
+ $s->cursor = mb_strlen((string) $s->value);
+ });
// Key: Deletion
- $this->input->bind('BACKSPACE', function ($state) {
+ $this->input->bind('BACKSPACE', function (State|string $state): void {
$cur = (int) $state->cursor;
- if ($cur === 0) return;
- $state->value = mb_substr((string)$state->value, 0, $cur - 1) . mb_substr((string)$state->value, $cur);
+ if ($cur === 0) {
+ return;
+ }
+ $state->value = mb_substr((string) $state->value, 0, $cur - 1) . mb_substr((string) $state->value, $cur);
$state->cursor = $cur - 1;
- $state->error = null;
+ $state->error = null;
});
- $this->input->bind('DELETE', function ($state) {
- $cur = (int) $state->cursor;
+ $this->input->bind('DELETE', function (State|string $state): void {
+ $cur = (int) $state->cursor;
$value = (string) $state->value;
- if ($cur >= mb_strlen($value)) return;
+ if ($cur >= mb_strlen($value)) {
+ return;
+ }
$state->value = mb_substr($value, 0, $cur) . mb_substr($value, $cur + 1);
});
// Key: Submission
- $this->input->bind('ENTER', function ($state) {
+ $this->input->bind('ENTER', function (State|string $state): void {
$value = (string) $state->value;
if ($value === '' && $this->defaultValue !== '') {
@@ -126,10 +136,10 @@ public function render(): void
Terminal::hideCursor();
- $value = (string) $this->state->value;
+ $value = (string) $this->state->value;
$cursor = (int) $this->state->cursor;
- $error = $this->state->error;
- $done = (bool) $this->state->done;
+ $error = $this->state->error;
+ $done = (bool) $this->state->done;
$lines = [];
@@ -140,13 +150,13 @@ public function render(): void
if (!$done) {
// Line 2: Input Area
$before = mb_substr($value, 0, $cursor);
- $at = mb_substr($value, $cursor, 1);
- $after = mb_substr($value, $cursor + 1);
- $char = ($at !== '') ? $at : ' ';
+ $at = mb_substr($value, $cursor, 1);
+ $after = mb_substr($value, $cursor + 1);
+ $char = ($at !== '') ? $at : ' ';
// Block Cursor using Inverse Video ANSI
$cursorAnsi = Colors::wrap($char, [Colors::BOLD, "\033[7m"]);
-
+
$displayText = Colors::wrap($before, Colors::YELLOW) . $cursorAnsi . Colors::wrap($after, Colors::YELLOW);
// Handle Placeholder
@@ -194,4 +204,4 @@ public function resolve(): mixed
$value = (string) $this->state->value;
return ($value !== '') ? $value : $this->defaultValue;
}
-}
\ No newline at end of file
+}
diff --git a/src/ConsoleIO.php b/src/ConsoleIO.php
index 62f1290..2de7d6a 100644
--- a/src/ConsoleIO.php
+++ b/src/ConsoleIO.php
@@ -1,4 +1,5 @@
doWrite($messages, $newline, false, $verbosity);
}
- public function writeError($messages, bool $newline = true, int $verbosity = self::NORMAL): void
+ public function writeError(mixed $messages, bool $newline = true, int $verbosity = self::NORMAL): void
{
$this->doWrite($messages, $newline, true, $verbosity);
}
- public function writeRaw($messages, bool $newline = true, int $verbosity = self::NORMAL): void
+ public function writeRaw(mixed $messages, bool $newline = true, int $verbosity = self::NORMAL): void
{
$this->doWrite($messages, $newline, false, $verbosity, raw: true);
}
- public function writeErrorRaw($messages, bool $newline = true, int $verbosity = self::NORMAL): void
+ public function writeErrorRaw(mixed $messages, bool $newline = true, int $verbosity = self::NORMAL): void
{
$this->doWrite($messages, $newline, true, $verbosity, raw: true);
}
@@ -279,7 +280,7 @@ private function doWrite(
$mem = round(memory_get_usage() / 1024 / 1024, 1);
$time = round(microtime(true) - $this->startTime, 2);
$prefix = Colors::muted("[{$mem}MiB/{$time}s] ");
- $messages = array_map(fn ($m) => $prefix . $m, $messages);
+ $messages = array_map(fn($m) => $prefix . $m, $messages);
}
$target = $stderr ? $this->getErrorOutput() : $this->output;
@@ -297,12 +298,12 @@ private function doWrite(
OVERWRITE
========================================================= */
- public function overwrite($messages, bool $newline = true, ?int $size = null, int $verbosity = self::NORMAL): void
+ public function overwrite(mixed $messages, bool $newline = true, ?int $size = null, int $verbosity = self::NORMAL): void
{
$this->doOverwrite($messages, $newline, $size, false, $verbosity);
}
- public function overwriteError($messages, bool $newline = true, ?int $size = null, int $verbosity = self::NORMAL): void
+ public function overwriteError(mixed $messages, bool $newline = true, ?int $size = null, int $verbosity = self::NORMAL): void
{
$this->doOverwrite($messages, $newline, $size, true, $verbosity);
}
@@ -341,9 +342,24 @@ public function getErrorOutput(): OutputInterface
: $this->output;
}
- public function isInteractive(): bool { return $this->input->isInteractive(); }
- public function isVerbose(): bool { return $this->output->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE; }
- public function isVeryVerbose(): bool { return $this->output->getVerbosity() >= OutputInterface::VERBOSITY_VERY_VERBOSE; }
- public function isDebug(): bool { return $this->output->getVerbosity() >= OutputInterface::VERBOSITY_DEBUG; }
- public function isDecorated(): bool { return $this->output->isDecorated(); }
-}
\ No newline at end of file
+ public function isInteractive(): bool
+ {
+ return $this->input->isInteractive();
+ }
+ public function isVerbose(): bool
+ {
+ return $this->output->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE;
+ }
+ public function isVeryVerbose(): bool
+ {
+ return $this->output->getVerbosity() >= OutputInterface::VERBOSITY_VERY_VERBOSE;
+ }
+ public function isDebug(): bool
+ {
+ return $this->output->getVerbosity() >= OutputInterface::VERBOSITY_DEBUG;
+ }
+ public function isDecorated(): bool
+ {
+ return $this->output->isDecorated();
+ }
+}
diff --git a/src/Depends/Colors.php b/src/Depends/Colors.php
index 952da41..b0f630f 100644
--- a/src/Depends/Colors.php
+++ b/src/Depends/Colors.php
@@ -1,4 +1,5 @@
$item) {
$score = self::score($query, (string) $item, $index);
-
+
// Only include items that meet a minimum relevancy threshold
if ($score > $minScore) {
$scored[$index] = $score;
@@ -27,7 +28,7 @@ public static function filter(array $items, string $query, int $minScore = 0): a
arsort($scored, SORT_NUMERIC);
return array_map(
- fn ($i) => $items[$i],
+ fn($i) => $items[$i],
array_keys($scored)
);
}
@@ -40,15 +41,19 @@ public static function score(string $query, string $value, int $tieBreaker = 0):
$query = mb_strtolower(trim($query));
$value = mb_strtolower(trim($value));
- if ($query === '') return 0;
- if ($query === $value) return 10000;
+ if ($query === '') {
+ return 0;
+ }
+ if ($query === $value) {
+ return 10000;
+ }
$score = 0;
// 1. Prefix Match
if (str_starts_with($value, $query)) {
$score = 9000 - mb_strlen($value);
- }
+ }
// 2. Substring Match
elseif (str_contains($value, $query)) {
$score = 8000 - mb_strlen($value);
@@ -57,20 +62,16 @@ public static function score(string $query, string $value, int $tieBreaker = 0):
elseif (self::isAbbreviation($query, $value)) {
$score = 7000 - mb_strlen($value);
}
+ // No other matches
+ else {
+ return -1 - $tieBreaker;
+ }
- // 4. Token-based match (multi-word)
+ // 4. Token-based match bonus (multi-word)
$queryTokens = explode(' ', $query);
$valueTokens = explode(' ', $value);
$score += self::tokenScore($queryTokens, $valueTokens);
- // 5. Levenshtein (Only if strings are within PHP's 255 char limit)
- if (mb_strlen($query) < 255 && mb_strlen($value) < 255) {
- $distance = levenshtein($query, $value);
- // If distance is large relative to length, it's a poor match
- $levScore = max(0, 2000 - ($distance * 20));
- $score += $levScore;
- }
-
// Final score minus tiebreaker for stable sorting
return $score - $tieBreaker;
}
@@ -82,15 +83,19 @@ private static function isAbbreviation(string $query, string $value): bool
{
$qLen = mb_strlen($query);
$vLen = mb_strlen($value);
-
- if ($qLen > $vLen) return false;
+
+ if ($qLen > $vLen) {
+ return false;
+ }
$qIdx = 0;
for ($vIdx = 0; $vIdx < $vLen; $vIdx++) {
if ($value[$vIdx] === $query[$qIdx]) {
$qIdx++;
}
- if ($qIdx === $qLen) return true;
+ if ($qIdx === $qLen) {
+ return true;
+ }
}
return false;
@@ -100,12 +105,17 @@ private static function tokenScore(array $queryTokens, array $valueTokens): int
{
$score = 0;
foreach ($queryTokens as $q) {
- if ($q === '') continue;
+ if ($q === '') {
+ continue;
+ }
foreach ($valueTokens as $v) {
- if ($q === $v) $score += 500;
- elseif (str_starts_with($v, $q)) $score += 200;
+ if ($q === $v) {
+ $score += 500;
+ } elseif (str_starts_with($v, $q)) {
+ $score += 200;
+ }
}
}
return $score;
}
-}
\ No newline at end of file
+}
diff --git a/src/Depends/Input.php b/src/Depends/Input.php
index ae4ffb2..a586c01 100644
--- a/src/Depends/Input.php
+++ b/src/Depends/Input.php
@@ -1,4 +1,5 @@
$keys
* @param Closure(State, string): (void|bool) $handler
*/
@@ -30,7 +31,7 @@ public function bind(string|array $keys, Closure $handler): self
/**
* Define what happens if no specific binding matches the key.
- *
+ *
* @param Closure(State, string): void $handler
*/
public function fallback(Closure $handler): self
@@ -56,7 +57,7 @@ public function unbind(string|array $keys): self
public function handle(string $key, State $state): void
{
// 1. Normalization: Map common hex/escape codes to readable strings
- $normalizedKey = Key::normalize($key);
+ $normalizedKey = Key::normalize($key);
if (isset($this->bindings[$normalizedKey])) {
foreach ($this->bindings[$normalizedKey] as $handler) {
@@ -76,4 +77,4 @@ public function handle(string $key, State $state): void
($this->fallback)($state, $key);
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Depends/Key.php b/src/Depends/Key.php
index 08ed066..9a0fd46 100644
--- a/src/Depends/Key.php
+++ b/src/Depends/Key.php
@@ -1,4 +1,5 @@
= 32;
+ // Control characters are < 32, or 127 (DEL)
+ // Printable ASCII is 32-126
+ if (mb_strlen($key) !== 1) {
+ return false;
+ }
+
+ $ord = ord($key);
+ return $ord >= 32 && $ord < 127;
}
-}
\ No newline at end of file
+}
diff --git a/src/Depends/RenderContext.php b/src/Depends/RenderContext.php
index 0fe2bc3..7ff6133 100644
--- a/src/Depends/RenderContext.php
+++ b/src/Depends/RenderContext.php
@@ -1,4 +1,5 @@
meta[$key] ?? $default;
}
-}
\ No newline at end of file
+}
diff --git a/src/Depends/Renderer.php b/src/Depends/Renderer.php
index 9abe54f..f2ceb8c 100644
--- a/src/Depends/Renderer.php
+++ b/src/Depends/Renderer.php
@@ -1,4 +1,5 @@
0) ? Colors::wrap(' ↑ more items', Colors::GRAY) : ' ';
- foreach (array_slice($filtered, $start, $windowSize) as $i => $label) {
+ foreach (array_values(array_slice($filtered, $start, $windowSize)) as $i => $label) {
$realIndex = $start + $i;
$isActive = $realIndex === $index;
$isSelected = in_array($label, (array) ($state->selected ?? []), true);
@@ -174,4 +175,4 @@ public function __destruct()
{
echo "\033[?25h";
}
-}
\ No newline at end of file
+}
diff --git a/src/Depends/Shell.php b/src/Depends/Shell.php
index 8ad9b07..39805ff 100644
--- a/src/Depends/Shell.php
+++ b/src/Depends/Shell.php
@@ -1,4 +1,5 @@
ok() ? trim($result->output()) : null;
}
-}
\ No newline at end of file
+}
diff --git a/src/Depends/ShellResult.php b/src/Depends/ShellResult.php
index be07c0d..4e34b8c 100644
--- a/src/Depends/ShellResult.php
+++ b/src/Depends/ShellResult.php
@@ -1,4 +1,5 @@
exitCode === 0; }
- public function failed(): bool { return $this->exitCode !== 0; }
+ public function ok(): bool
+ {
+ return $this->exitCode === 0;
+ }
+ public function failed(): bool
+ {
+ return $this->exitCode !== 0;
+ }
/** All stdout lines joined with newlines. */
public function output(): string
@@ -40,7 +47,7 @@ public function meaningfulErrors(): array
{
return array_values(array_filter(
$this->stderr,
- fn (string $l) => trim($l) !== ''
+ fn(string $l) => trim($l) !== ''
));
}
-}
\ No newline at end of file
+}
diff --git a/src/Depends/Spinner.php b/src/Depends/Spinner.php
index e398fcd..45a8293 100644
--- a/src/Depends/Spinner.php
+++ b/src/Depends/Spinner.php
@@ -1,4 +1,5 @@
currentFrame;
}
-}
\ No newline at end of file
+}
diff --git a/src/Depends/SpinnerFrames.php b/src/Depends/SpinnerFrames.php
index eb0abce..03729fb 100644
--- a/src/Depends/SpinnerFrames.php
+++ b/src/Depends/SpinnerFrames.php
@@ -1,4 +1,5 @@
set($key, array_values($current));
}
+ /**
+ * Filter items by search query (case-insensitive substring match).
+ * Returns all items if no search query is set.
+ *
+ * @return array
+ */
+ public function filtered(): array
+ {
+ $items = (array) $this->get('items', []);
+ $search = mb_strtolower((string) $this->get('search', ''));
+
+ if ($search === '') {
+ return $items;
+ }
+
+ return array_filter(
+ $items,
+ fn($item) => mb_stripos((string) $item, $search) !== false
+ );
+ }
+
/* --- Reactivity --- */
/**
@@ -121,4 +143,4 @@ private function notify(string $key, mixed $new, mixed $old): void
$cb($new, $old, $this);
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Depends/Terminal.php b/src/Depends/Terminal.php
index 848767d..7428f58 100644
--- a/src/Depends/Terminal.php
+++ b/src/Depends/Terminal.php
@@ -1,4 +1,5 @@
listeners[$event] = array_values(array_filter(
$this->listeners[$event],
- fn ($l) => $l !== $listener
+ fn($l) => $l !== $listener
));
return $this;
@@ -79,4 +80,4 @@ public function dispatchUntil(string $event, mixed $payload = null): mixed
return null;
}
-}
\ No newline at end of file
+}
diff --git a/src/ILifecycle.php b/src/ILifecycle.php
index e160c2b..3b3b0b8 100644
--- a/src/ILifecycle.php
+++ b/src/ILifecycle.php
@@ -1,4 +1,5 @@
: string|int|bool)
*/
public function select(
- string $question,
- array $choices,
- mixed $default,
- bool|int $attempts = false,
- string $errorMessage = 'Value "%s" is invalid',
+ string $question,
+ array $choices,
+ mixed $default,
+ bool|int $attempts = false,
+ string $errorMessage = 'Value "%s" is invalid',
bool $multiselect = false
): int|string|array|bool;
-}
\ No newline at end of file
+}
diff --git a/src/IPromptComponent.php b/src/IPromptComponent.php
index 39c176e..9c60406 100644
--- a/src/IPromptComponent.php
+++ b/src/IPromptComponent.php
@@ -1,4 +1,5 @@
execute([], $io); // populate io
- ob_start();
+ // execute() wires $this->io inside the command; printHelp() uses the
+ // same io, so all output lands in BufferIO — not in PHP's output buffer.
+ $cmd->execute(['placeholder'], $io);
$cmd->printHelp();
- $help = ob_get_clean();
- $this->assertStringContainsString('echo', (string)$help);
+ // FIX: was ob_start()/ob_get_clean() which captured PHP stdout (always
+ // empty here). Read from the BufferIO stream instead.
+ $help = $io->getOutput();
+
+ $this->assertStringContainsString('echo', $help);
}
}
diff --git a/tests/Integration/BufferIOTest.php b/tests/Integration/BufferIOTest.php
index 3d1d2e0..d20dd7c 100644
--- a/tests/Integration/BufferIOTest.php
+++ b/tests/Integration/BufferIOTest.php
@@ -1,4 +1,5 @@
name = 'boom'; }
- protected function handle(): int { throw new \RuntimeException('Boom!'); }
+ protected function configure(): void
+ {
+ $this->name = 'boom';
+ }
+ protected function handle(): int
+ {
+ throw new \RuntimeException('Boom!');
+ }
};
$app = (new CLIApplication())
diff --git a/tests/Unit/ColorsTest.php b/tests/Unit/ColorsTest.php
index bdc0134..9838305 100644
--- a/tests/Unit/ColorsTest.php
+++ b/tests/Unit/ColorsTest.php
@@ -1,4 +1,5 @@
hooks->on('event', function () use (&$log): void { $log[] = 'A'; });
- $this->hooks->on('event', function () use (&$log): void { $log[] = 'B'; });
- $this->hooks->on('event', function () use (&$log): void { $log[] = 'C'; });
+ $this->hooks->on('event', function () use (&$log): void {
+ $log[] = 'A';
+ });
+ $this->hooks->on('event', function () use (&$log): void {
+ $log[] = 'B';
+ });
+ $this->hooks->on('event', function () use (&$log): void {
+ $log[] = 'C';
+ });
$this->hooks->dispatch('event');
@@ -110,8 +117,12 @@ public function test_off_without_listener_removes_all(): void
{
$count = 0;
- $this->hooks->on('event', function () use (&$count): void { $count++; });
- $this->hooks->on('event', function () use (&$count): void { $count++; });
+ $this->hooks->on('event', function () use (&$count): void {
+ $count++;
+ });
+ $this->hooks->on('event', function () use (&$count): void {
+ $count++;
+ });
$this->hooks->off('event');
$this->hooks->dispatch('event');
diff --git a/tests/Unit/InputTest.php b/tests/Unit/InputTest.php
index 412b1b7..b9c0cec 100644
--- a/tests/Unit/InputTest.php
+++ b/tests/Unit/InputTest.php
@@ -1,4 +1,5 @@
["\e[H", 'HOME'],
'end' => ["\e[F", 'END'],
'enter (newline)' => ["\n", 'ENTER'],
- 'enter (carriage)'=> ["\r", 'ENTER'],
+ 'enter (carriage)' => ["\r", 'ENTER'],
'tab' => ["\t", 'TAB'],
'esc' => ["\e", 'ESC'],
'backspace (del)' => ["\x7f", 'BACKSPACE'],
diff --git a/tests/Unit/NullIOTest.php b/tests/Unit/NullIOTest.php
index 4f41c66..a9890c9 100644
--- a/tests/Unit/NullIOTest.php
+++ b/tests/Unit/NullIOTest.php
@@ -1,4 +1,5 @@
0]);
$calls = [];
- $state->watch('x', function () use (&$calls): void { $calls[] = 'first'; });
- $state->watch('x', function () use (&$calls): void { $calls[] = 'second'; });
+ $state->watch('x', function () use (&$calls): void {
+ $calls[] = 'first';
+ });
+ $state->watch('x', function () use (&$calls): void {
+ $calls[] = 'second';
+ });
$state->x = 99;
diff --git a/tests/Unit/TableTest.php b/tests/Unit/TableTest.php
index 26d2271..12a55cd 100644
--- a/tests/Unit/TableTest.php
+++ b/tests/Unit/TableTest.php
@@ -1,4 +1,5 @@