update lock clucknut
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 14s
Build, Push and Deploy / build-and-push (push) Successful in 3m14s
Build, Push and Deploy / deploy-staging (push) Successful in 25s
Build, Push and Deploy / deploy-production (push) Has been skipped

This commit is contained in:
2026-04-18 20:32:18 +07:00
parent 4554035227
commit dcaf267458
3359 changed files with 153185 additions and 205489 deletions

View File

@@ -3,7 +3,7 @@
/*
* This file is part of Psy Shell.
*
* (c) 2012-2025 Justin Hileman
* (c) 2012-2026 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.

View File

@@ -3,7 +3,7 @@
/*
* This file is part of Psy Shell.
*
* (c) 2012-2025 Justin Hileman
* (c) 2012-2026 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.

View File

@@ -3,7 +3,7 @@
/*
* This file is part of Psy Shell.
*
* (c) 2012-2025 Justin Hileman
* (c) 2012-2026 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -82,11 +82,11 @@ public function doWrite($message, $newline): void
*/
public function close()
{
if (isset($this->pipe)) {
if (\is_resource($this->pipe)) {
\fclose($this->pipe);
}
if (isset($this->proc)) {
if (\is_resource($this->proc)) {
$exit = \proc_close($this->proc);
if ($exit !== 0) {
throw new \RuntimeException('Error closing output stream');

View File

@@ -3,7 +3,7 @@
/*
* This file is part of Psy Shell.
*
* (c) 2012-2025 Justin Hileman
* (c) 2012-2026 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -26,6 +26,8 @@ class ShellOutput extends ConsoleOutput
private int $paging = 0;
private OutputPager $pager;
private Theme $theme;
/** @var callable|null */
private $writeListener = null;
/**
* Construct a ShellOutput instance.
@@ -42,15 +44,7 @@ public function __construct($verbosity = self::VERBOSITY_NORMAL, $decorated = nu
$this->theme = $theme ?? new Theme('modern');
$this->initFormatters();
if ($pager === null) {
$this->pager = new PassthruPager($this);
} elseif (\is_string($pager)) {
$this->pager = new ProcOutputPager($this, $pager);
} elseif ($pager instanceof OutputPager) {
$this->pager = $pager;
} else {
throw new \InvalidArgumentException('Unexpected pager parameter: '.$pager);
}
$this->pager = $this->createPager($pager);
}
/**
@@ -89,6 +83,14 @@ public function page($messages, int $type = 0)
$this->stopPaging();
}
/**
* Set a listener invoked whenever visible output is written.
*/
public function setWriteListener(?callable $listener): void
{
$this->writeListener = $listener;
}
/**
* Start sending output to the output pager.
*/
@@ -128,14 +130,33 @@ public function write($messages, $newline = false, $type = 0): void
if ($type & self::NUMBER_LINES) {
$pad = \strlen((string) \count($messages));
$template = $this->isDecorated() ? "<aside>%{$pad}s</aside>: %s" : "%{$pad}s: %s";
$template = $this->isDecorated() && $this->getFormatter()->hasStyle('whisper')
? "<whisper>%{$pad}s:</whisper> %s"
: "%{$pad}s: %s";
if ($type & self::OUTPUT_RAW) {
$messages = \array_map([OutputFormatter::class, 'escape'], $messages);
}
$indent = \str_repeat(' ', $pad + 2); // Indent continuation lines to align with text
foreach ($messages as $i => $line) {
$messages[$i] = \sprintf($template, $i, $line);
// Check if line contains newlines (multi-line entry)
if (\strpos($line, "\n") !== false) {
// Split into lines and indent continuation lines
$lines = \explode("\n", $line);
$firstLine = \array_shift($lines);
$indentedLines = \array_map(function ($l) use ($indent) {
return $indent.$l;
}, $lines);
$messages[$i] = \sprintf($template, $i, $firstLine);
if (!empty($indentedLines)) {
$messages[$i] .= "\n".\implode("\n", $indentedLines);
}
} else {
$messages[$i] = \sprintf($template, $i, $line);
}
}
// clean this up for super.
@@ -155,6 +176,10 @@ public function write($messages, $newline = false, $type = 0): void
*/
public function doWrite($message, $newline): void
{
if ($this->writeListener) {
($this->writeListener)();
}
// @todo Update OutputPager interface to require doWrite
if ($this->paging > 0 && ($this->pager instanceof ProcOutputPager || $this->pager instanceof PassthruPager)) {
$this->pager->doWrite($message, $newline);
@@ -172,6 +197,18 @@ public function setTheme(Theme $theme)
$this->initFormatters();
}
/**
* Replace the output pager used for future paging operations.
*
* @param string|OutputPager|null $pager
*/
public function setPager($pager): void
{
$this->closePager();
$this->paging = 0;
$this->pager = $this->createPager($pager);
}
/**
* Flush and close the output pager.
*/
@@ -182,6 +219,26 @@ private function closePager()
}
}
/**
* @param string|OutputPager|null $pager
*/
private function createPager($pager): OutputPager
{
if ($pager === null) {
return new PassthruPager($this);
}
if (\is_string($pager)) {
return new ProcOutputPager($this, $pager);
}
if ($pager instanceof OutputPager) {
return $pager;
}
throw new \InvalidArgumentException('Unexpected pager parameter: '.$pager);
}
/**
* Initialize output formatter styles.
*/

View File

@@ -0,0 +1,249 @@
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2026 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\Output;
use Symfony\Component\Console\Formatter\OutputFormatter;
use Symfony\Component\Console\Formatter\OutputFormatterInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Adapter for output methods provided by ShellOutput but used by commands.
*
* This allows commands to use paging and line numbering with any
* OutputInterface implementation.
*
* @todo On the next major release, consider removing ShellOutput and folding
* its functionality entirely into this adapter.
*/
class ShellOutputAdapter implements OutputInterface
{
// @todo Remove ShellOutput::NUMBER_LINES alias on next major release.
public const NUMBER_LINES = ShellOutput::NUMBER_LINES;
private OutputInterface $output;
/**
* @param OutputInterface $output
*/
public function __construct(OutputInterface $output)
{
$this->output = $output;
}
/**
* Page multiple lines of output.
*
* @param string|array|\Closure $messages
*/
public function page($messages, int $type = 0): void
{
if ($this->output instanceof ShellOutput) {
$this->output->page($messages, $type);
return;
}
if (\is_string($messages)) {
// Split on newlines to avoid O(n^2) performance in Symfony's OutputFormatter
// when processing large strings with many style tags.
$messages = \explode("\n", $messages);
}
if (!\is_array($messages) && !\is_callable($messages)) {
throw new \InvalidArgumentException('Paged output requires a string, array or callback');
}
$this->startPaging();
if (\is_callable($messages)) {
$messages($this);
} else {
$this->write($messages, true, $type);
}
$this->stopPaging();
}
/**
* Start sending output to the output pager.
*/
public function startPaging(): void
{
if ($this->output instanceof ShellOutput) {
$this->output->startPaging();
}
}
/**
* Stop paging output and flush the output pager.
*/
public function stopPaging(): void
{
if ($this->output instanceof ShellOutput) {
$this->output->stopPaging();
}
}
/**
* Writes a message to the output.
*
* @param string|array $messages
*/
public function write($messages, $newline = false, $type = 0): void
{
if ($this->output instanceof ShellOutput) {
$this->output->write($messages, $newline, $type);
return;
}
$messages = (array) $messages;
if ($type & self::NUMBER_LINES) {
$messages = $this->numberLines($messages, $type);
// clean this up for the wrapped output.
$type = $type & ~self::NUMBER_LINES & ~OutputInterface::OUTPUT_RAW;
}
$this->output->write($messages, $newline, $type);
}
/**
* Writes a message to the output and adds a newline at the end.
*
* @param string|array $messages
*/
public function writeln($messages, $type = 0): void
{
$this->write($messages, true, $type);
}
/**
* {@inheritdoc}
*/
public function setVerbosity($level): void
{
$this->output->setVerbosity($level);
}
/**
* {@inheritdoc}
*/
public function getVerbosity(): int
{
return $this->output->getVerbosity();
}
/**
* {@inheritdoc}
*/
public function isQuiet(): bool
{
return $this->output->isQuiet();
}
/**
* {@inheritdoc}
*/
public function isVerbose(): bool
{
return $this->output->isVerbose();
}
/**
* {@inheritdoc}
*/
public function isVeryVerbose(): bool
{
return $this->output->isVeryVerbose();
}
/**
* @todo Remove method_exists guard when dropping support for Symfony < 7.2.
*
* @suppress PhanUndeclaredMethod
*/
public function isSilent(): bool
{
if (\method_exists($this->output, 'isSilent')) {
return $this->output->isSilent();
}
return false;
}
/**
* {@inheritdoc}
*/
public function isDebug(): bool
{
return $this->output->isDebug();
}
/**
* {@inheritdoc}
*/
public function setDecorated($decorated): void
{
$this->output->setDecorated($decorated);
}
/**
* {@inheritdoc}
*/
public function isDecorated(): bool
{
return $this->output->isDecorated();
}
/**
* {@inheritdoc}
*/
public function setFormatter(OutputFormatterInterface $formatter): void
{
$this->output->setFormatter($formatter);
}
/**
* {@inheritdoc}
*/
public function getFormatter(): OutputFormatterInterface
{
return $this->output->getFormatter();
}
/**
* Add line numbers to a message array.
*
* @param string[] $messages
*
* @return string[]
*/
private function numberLines(array $messages, int $type): array
{
$pad = \strlen((string) \count($messages));
$template = $this->output->isDecorated() && $this->output->getFormatter()->hasStyle('whisper')
? "<whisper>%{$pad}s:</whisper> %s"
: "%{$pad}s: %s";
if ($type & OutputInterface::OUTPUT_RAW) {
$messages = \array_map([OutputFormatter::class, 'escape'], $messages);
}
foreach ($messages as $i => $line) {
$messages[$i] = \sprintf($template, $i, $line);
}
return $messages;
}
}

View File

@@ -3,7 +3,7 @@
/*
* This file is part of Psy Shell.
*
* (c) 2012-2025 Justin Hileman
* (c) 2012-2026 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -19,6 +19,8 @@
*/
class Theme
{
public const BUILTIN_THEMES = ['modern', 'compact', 'classic'];
const MODERN_THEME = []; // Defaults :)
const COMPACT_THEME = [
@@ -34,19 +36,29 @@ class Theme
'returnValue' => '=> ',
];
private const BUILTIN_THEME_CONFIGS = [
'modern' => self::MODERN_THEME,
'compact' => self::COMPACT_THEME,
'classic' => self::CLASSIC_THEME,
];
// Custom themes fall back to DEFAULT_STYLES for any undefined style.
const DEFAULT_STYLES = [
'info' => ['white', 'blue', ['bold']],
'info' => ['green', null, ['bold']],
'warning' => ['black', 'yellow'],
'error' => ['white', 'red', ['bold']],
'whisper' => ['gray'],
'aside' => ['blue'],
'strong' => [null, null, ['bold']],
'return' => ['cyan'],
'urgent' => ['red'],
'hidden' => ['black'],
'aside' => ['blue'],
'strong' => [null, null, ['bold']],
'return' => ['cyan'],
'urgent' => ['red'],
'hidden' => ['black'],
'command' => ['cyan', null, ['bold']],
'command_option' => ['yellow'],
'command_argument' => ['green'],
// Visibility
// Keywords
'public' => [null, null, ['bold']],
'protected' => ['yellow'],
'private' => ['red'],
@@ -54,13 +66,15 @@ class Theme
'const' => ['cyan'],
'class' => ['blue', null, ['underscore']],
'function' => [null],
'virtual' => ['magenta'],
'default' => [null],
// Types
'number' => ['magenta'],
'integer' => ['magenta'],
'float' => ['yellow'],
'float' => ['magenta'],
'string' => ['green'],
'array_key' => ['blue'],
'bool' => ['cyan'],
'keyword' => ['yellow'],
'comment' => ['blue'],
@@ -70,6 +84,11 @@ class Theme
// Code-specific formatting
'inline_html' => ['cyan'],
// Interactive readline
'input_frame' => ['bright-white', 'gray'],
'input_frame_error' => ['bright-white', 'red'],
'input_highlight' => [null, null, ['reverse']],
];
const ERROR_STYLES = ['info', 'warning', 'error', 'whisper', 'class'];
@@ -91,23 +110,11 @@ class Theme
public function __construct($config = 'modern')
{
if (\is_string($config)) {
switch ($config) {
case 'modern':
$config = static::MODERN_THEME;
break;
case 'compact':
$config = static::COMPACT_THEME;
break;
case 'classic':
$config = static::CLASSIC_THEME;
break;
default:
\trigger_error(\sprintf('Unknown theme: %s', $config), \E_USER_NOTICE);
$config = static::MODERN_THEME;
break;
if (isset(self::BUILTIN_THEME_CONFIGS[$config])) {
$config = self::BUILTIN_THEME_CONFIGS[$config];
} else {
\trigger_error(\sprintf('Unknown theme: %s', $config), \E_USER_NOTICE);
$config = static::MODERN_THEME;
}
}
@@ -253,6 +260,34 @@ public function setStyles(array $styles)
}
}
/**
* Get the built-in theme name, or null for custom themes.
*/
public function getName(): ?string
{
foreach (self::BUILTIN_THEMES as $name) {
if ($this->equals(new self($name))) {
return $name;
}
}
return null;
}
/**
* Compare two themes by effective config.
*/
public function equals(self $theme): bool
{
return $this->compact === $theme->compact
&& $this->prompt === $theme->prompt
&& $this->bufferPrompt === $theme->bufferPrompt
&& $this->replayPrompt === $theme->replayPrompt
&& $this->returnValue === $theme->returnValue
&& $this->grayFallback === $theme->grayFallback
&& $this->styles === $theme->styles;
}
/**
* Apply the current output formatter styles.
*/
@@ -280,9 +315,18 @@ public function applyErrorStyles(OutputFormatterInterface $errorFormatter, bool
*/
private function getStyle(string $name, bool $useGrayFallback): array
{
return \array_map(function ($style) use ($useGrayFallback) {
return ($useGrayFallback && $style === 'gray') ? $this->grayFallback : $style;
}, $this->styles[$name]);
if (!$useGrayFallback) {
return $this->styles[$name];
}
// The default input_frame styles use extended colors (bright-white,
// gray) unavailable on older Symfony Console. Drop them rather than
// falling back to unreadable backgrounds.
if (($name === 'input_frame' || $name === 'input_frame_error') && $this->styles[$name] === static::DEFAULT_STYLES[$name]) {
return [null, null];
}
return \array_map(fn ($style) => ($style === 'gray') ? $this->grayFallback : $style, $this->styles[$name]);
}
/**