update lock clucknut
All checks were successful
All checks were successful
This commit is contained in:
@@ -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.
|
||||
|
||||
493
vendor/psy/psysh/src/Util/Docblock.php
vendored
493
vendor/psy/psysh/src/Util/Docblock.php
vendored
@@ -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.
|
||||
@@ -11,6 +11,9 @@
|
||||
|
||||
namespace Psy\Util;
|
||||
|
||||
use Psy\Reflection\ReflectionMagicMethod;
|
||||
use Psy\Reflection\ReflectionMagicProperty;
|
||||
|
||||
/**
|
||||
* A docblock representation.
|
||||
*
|
||||
@@ -36,6 +39,12 @@ class Docblock
|
||||
'return' => ['type', 'desc'],
|
||||
];
|
||||
|
||||
/** @var array<string, ReflectionMagicMethod[]> Cache for getMagicMethods() */
|
||||
private static array $methodCache = [];
|
||||
|
||||
/** @var array<string, ReflectionMagicProperty[]> Cache for getMagicProperties() */
|
||||
private static array $propertyCache = [];
|
||||
|
||||
protected $reflector;
|
||||
|
||||
/**
|
||||
@@ -104,9 +113,7 @@ protected function setComment(string $comment)
|
||||
protected static function prefixLength(array $lines): int
|
||||
{
|
||||
// find only lines with interesting things
|
||||
$lines = \array_filter($lines, function ($line) {
|
||||
return \substr($line, \strspn($line, "* \t\n\r\0\x0B"));
|
||||
});
|
||||
$lines = \array_filter($lines, fn ($line) => \substr($line, \strspn($line, "* \t\n\r\0\x0B")));
|
||||
|
||||
// if we sort the lines, we only have to compare two items
|
||||
\sort($lines);
|
||||
@@ -119,10 +126,10 @@ protected static function prefixLength(array $lines): int
|
||||
return \strspn($first, "* \t\n\r\0\x0B");
|
||||
}
|
||||
|
||||
// find the longest common substring
|
||||
// find the longest common substring, but stop before @ (tag marker)
|
||||
$count = \min(\strlen($first), \strlen($last));
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
if ($first[$i] !== $last[$i]) {
|
||||
if ($first[$i] !== $last[$i] || $first[$i] === '@') {
|
||||
return $i;
|
||||
}
|
||||
}
|
||||
@@ -145,9 +152,7 @@ protected function parseComment(string $comment)
|
||||
|
||||
// Trim asterisks and whitespace from the beginning and whitespace from the end of lines
|
||||
$prefixLength = self::prefixLength($comment);
|
||||
$comment = \array_map(function ($line) use ($prefixLength) {
|
||||
return \rtrim(\substr($line, $prefixLength));
|
||||
}, $comment);
|
||||
$comment = \array_map(fn ($line) => \rtrim(\substr($line, $prefixLength)), $comment);
|
||||
|
||||
// Group the lines together by @tags
|
||||
$blocks = [];
|
||||
@@ -179,7 +184,7 @@ protected function parseComment(string $comment)
|
||||
// The tagged block is a vector
|
||||
$count = \count(self::$vectors[$tag]);
|
||||
if ($body) {
|
||||
$parts = \preg_split('/\s+/', $body, $count);
|
||||
$parts = self::splitOnWhitespace($body, $count);
|
||||
} else {
|
||||
$parts = [];
|
||||
}
|
||||
@@ -244,4 +249,472 @@ public static function strTag(string $str)
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Split a string on whitespace, respecting balanced brackets.
|
||||
*
|
||||
* Like preg_split('/\s+/', $str, $limit) but treats `<...>` and `(...)` as atomic,
|
||||
* so `array<int, string> $foo` splits into ['array<int, string>', '$foo']
|
||||
* and `find(int $id)` stays as one token instead of being split.
|
||||
*
|
||||
* Uses a stack to ensure proper nesting (e.g. `<(>)` won't break).
|
||||
*
|
||||
* @param string $str The string to split
|
||||
* @param int $limit Maximum number of parts (0 = unlimited)
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
private static function splitOnWhitespace(string $str, int $limit = 0): array
|
||||
{
|
||||
static $brackets = ['<' => '>', '(' => ')'];
|
||||
|
||||
$parts = [];
|
||||
$current = '';
|
||||
$stack = [];
|
||||
$len = \strlen($str);
|
||||
$i = 0;
|
||||
|
||||
while ($i < $len) {
|
||||
$char = $str[$i];
|
||||
|
||||
if (isset($brackets[$char])) {
|
||||
// Opening bracket; push onto stack
|
||||
$stack[] = $char;
|
||||
$current .= $char;
|
||||
} elseif (!empty($stack) && $brackets[\end($stack)] === $char) {
|
||||
// Closing bracket matching the current opener; pop stack
|
||||
\array_pop($stack);
|
||||
$current .= $char;
|
||||
} elseif (empty($stack) && \ctype_space($char)) {
|
||||
// At top level, whitespace is a delimiter
|
||||
if ($current !== '') {
|
||||
$parts[] = $current;
|
||||
$current = '';
|
||||
|
||||
// If we've hit the limit, grab the rest as the final part
|
||||
if ($limit > 0 && \count($parts) === $limit - 1) {
|
||||
$rest = \ltrim(\substr($str, $i));
|
||||
if ($rest !== '') {
|
||||
$parts[] = $rest;
|
||||
}
|
||||
|
||||
return $parts;
|
||||
}
|
||||
}
|
||||
// Skip consecutive whitespace
|
||||
while ($i + 1 < $len && \ctype_space($str[$i + 1])) {
|
||||
$i++;
|
||||
}
|
||||
} else {
|
||||
$current .= $char;
|
||||
}
|
||||
|
||||
$i++;
|
||||
}
|
||||
|
||||
if ($current !== '') {
|
||||
$parts[] = $current;
|
||||
}
|
||||
|
||||
return $parts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get magic methods declared in this docblock via @method tags.
|
||||
*
|
||||
* Only works when the docblock was created from a ReflectionClass.
|
||||
*
|
||||
* @return ReflectionMagicMethod[]
|
||||
*/
|
||||
public function getMethods(): array
|
||||
{
|
||||
if (empty($this->comment) || !$this->reflector instanceof \ReflectionClass) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return self::parseMethodTags($this->comment, $this->reflector);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get magic properties declared in this docblock via @property tags.
|
||||
*
|
||||
* Includes property, property-read, and property-write tags.
|
||||
* Only works when the docblock was created from a ReflectionClass.
|
||||
*
|
||||
* @return ReflectionMagicProperty[]
|
||||
*/
|
||||
public function getProperties(): array
|
||||
{
|
||||
if (empty($this->comment) || !$this->reflector instanceof \ReflectionClass) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return self::parsePropertyTags($this->comment, $this->reflector);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all magic methods from a class and its parents, interfaces, and traits.
|
||||
*
|
||||
* Results are cached for performance.
|
||||
*
|
||||
* @return ReflectionMagicMethod[]
|
||||
*/
|
||||
public static function getMagicMethods(\ReflectionClass $class): array
|
||||
{
|
||||
$cacheKey = $class->getName();
|
||||
|
||||
if (isset(self::$methodCache[$cacheKey])) {
|
||||
return self::$methodCache[$cacheKey];
|
||||
}
|
||||
|
||||
$methods = [];
|
||||
|
||||
// Walk parent classes first (so child methods take precedence)
|
||||
$parent = $class->getParentClass();
|
||||
if ($parent !== false) {
|
||||
foreach (self::getMagicMethods($parent) as $method) {
|
||||
$methods[$method->getName()] = $method;
|
||||
}
|
||||
}
|
||||
|
||||
// Walk interfaces
|
||||
foreach ($class->getInterfaces() as $interface) {
|
||||
foreach (self::getMagicMethods($interface) as $method) {
|
||||
if (!isset($methods[$method->getName()])) {
|
||||
$methods[$method->getName()] = $method;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Walk traits
|
||||
foreach ($class->getTraits() as $trait) {
|
||||
foreach (self::getMagicMethods($trait) as $method) {
|
||||
if (!isset($methods[$method->getName()])) {
|
||||
$methods[$method->getName()] = $method;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse this class's docblock (takes precedence over inherited)
|
||||
$docComment = $class->getDocComment();
|
||||
if ($docComment !== false) {
|
||||
foreach (self::parseMethodTags($docComment, $class) as $method) {
|
||||
$methods[$method->getName()] = $method;
|
||||
}
|
||||
}
|
||||
|
||||
self::$methodCache[$cacheKey] = \array_values($methods);
|
||||
|
||||
return self::$methodCache[$cacheKey];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all magic properties from a class and its parents, interfaces, and traits.
|
||||
*
|
||||
* Results are cached for performance.
|
||||
*
|
||||
* @return ReflectionMagicProperty[]
|
||||
*/
|
||||
public static function getMagicProperties(\ReflectionClass $class): array
|
||||
{
|
||||
$cacheKey = $class->getName();
|
||||
|
||||
if (isset(self::$propertyCache[$cacheKey])) {
|
||||
return self::$propertyCache[$cacheKey];
|
||||
}
|
||||
|
||||
$properties = [];
|
||||
|
||||
// Walk parent classes first (so child properties take precedence)
|
||||
$parent = $class->getParentClass();
|
||||
if ($parent !== false) {
|
||||
foreach (self::getMagicProperties($parent) as $property) {
|
||||
$properties[$property->getName()] = $property;
|
||||
}
|
||||
}
|
||||
|
||||
// Walk interfaces
|
||||
foreach ($class->getInterfaces() as $interface) {
|
||||
foreach (self::getMagicProperties($interface) as $property) {
|
||||
if (!isset($properties[$property->getName()])) {
|
||||
$properties[$property->getName()] = $property;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Walk traits
|
||||
foreach ($class->getTraits() as $trait) {
|
||||
foreach (self::getMagicProperties($trait) as $property) {
|
||||
if (!isset($properties[$property->getName()])) {
|
||||
$properties[$property->getName()] = $property;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse this class's docblock (takes precedence over inherited)
|
||||
$docComment = $class->getDocComment();
|
||||
if ($docComment !== false) {
|
||||
foreach (self::parsePropertyTags($docComment, $class) as $property) {
|
||||
$properties[$property->getName()] = $property;
|
||||
}
|
||||
}
|
||||
|
||||
self::$propertyCache[$cacheKey] = \array_values($properties);
|
||||
|
||||
return self::$propertyCache[$cacheKey];
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the magic method and property caches.
|
||||
*
|
||||
* Useful for testing or when classes are redefined.
|
||||
*/
|
||||
public static function clearMagicCache(): void
|
||||
{
|
||||
self::$methodCache = [];
|
||||
self::$propertyCache = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a single @method tag body.
|
||||
*
|
||||
* Handles: [static] [returnType] [&]methodName([params]) [description]
|
||||
*
|
||||
* @return array|null Parsed method data or null if invalid
|
||||
*/
|
||||
private static function parseMethodTag(string $body): ?array
|
||||
{
|
||||
$body = \trim($body);
|
||||
if ($body === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$isStatic = false;
|
||||
$returnType = null;
|
||||
$returnsReference = false;
|
||||
$name = null;
|
||||
$parameters = '';
|
||||
$description = null;
|
||||
|
||||
// Check for 'static' keyword at the start
|
||||
if (\strpos($body, 'static ') === 0) {
|
||||
$isStatic = true;
|
||||
$body = \ltrim(\substr($body, 7));
|
||||
}
|
||||
|
||||
// Split into tokens respecting generics
|
||||
$tokens = self::splitOnWhitespace($body);
|
||||
if (empty($tokens)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Find the method name (contains parentheses or is the last non-description token)
|
||||
$methodIndex = null;
|
||||
foreach ($tokens as $i => $token) {
|
||||
if (\preg_match('/^&?[\w_]+\s*\(/', $token)) {
|
||||
$methodIndex = $i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If no parentheses found, check for bare method name pattern
|
||||
if ($methodIndex === null) {
|
||||
foreach ($tokens as $i => $token) {
|
||||
if (\preg_match('/^&?[\w_]+$/', $token) && $i > 0) {
|
||||
// Could be a method name if previous tokens look like types
|
||||
$methodIndex = $i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If still no method found, first token might be the method name
|
||||
if ($methodIndex === null) {
|
||||
if (\preg_match('/^&?[\w_]+/', $tokens[0])) {
|
||||
$methodIndex = 0;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Everything before methodIndex is the return type
|
||||
if ($methodIndex > 0) {
|
||||
$returnType = \implode(' ', \array_slice($tokens, 0, $methodIndex));
|
||||
}
|
||||
|
||||
// Parse the method token
|
||||
$methodToken = $tokens[$methodIndex];
|
||||
|
||||
// Check for returns-by-reference
|
||||
if (\strpos($methodToken, '&') === 0) {
|
||||
$returnsReference = true;
|
||||
$methodToken = \substr($methodToken, 1);
|
||||
}
|
||||
|
||||
// Extract method name and parameters
|
||||
if (\preg_match('/^([\w_]+)\s*(?:\(([^\)]*)\))?/', $methodToken, $matches)) {
|
||||
$name = $matches[1];
|
||||
$parameters = $matches[2] ?? '';
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Everything after the method token is the description
|
||||
if ($methodIndex < \count($tokens) - 1) {
|
||||
$descParts = \array_slice($tokens, $methodIndex + 1);
|
||||
$description = \implode(' ', $descParts);
|
||||
}
|
||||
|
||||
return [
|
||||
'name' => $name,
|
||||
'static' => $isStatic,
|
||||
'returnType' => $returnType,
|
||||
'returnsReference' => $returnsReference,
|
||||
'parameters' => $parameters,
|
||||
'description' => $description !== '' ? $description : null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a single @property tag body.
|
||||
*
|
||||
* Handles: [type] $name [description]
|
||||
*
|
||||
* @param string $tagName One of 'property', 'property-read', 'property-write'
|
||||
*
|
||||
* @return array|null Parsed property data or null if invalid
|
||||
*/
|
||||
private static function parsePropertyTag(string $body, string $tagName): ?array
|
||||
{
|
||||
$body = \trim($body);
|
||||
if ($body === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$type = null;
|
||||
$name = null;
|
||||
$description = null;
|
||||
|
||||
// Split into tokens respecting generics
|
||||
$tokens = self::splitOnWhitespace($body);
|
||||
if (empty($tokens)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Find the property name (starts with $ or is a bare word after a type)
|
||||
$nameIndex = null;
|
||||
foreach ($tokens as $i => $token) {
|
||||
if (\strpos($token, '$') === 0) {
|
||||
$nameIndex = $i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If no $ found, check if second token looks like a name (first would be type)
|
||||
if ($nameIndex === null && \count($tokens) >= 2) {
|
||||
if (\preg_match('/^[\w_]+$/', $tokens[1])) {
|
||||
$nameIndex = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// If still no name found, first token is the name (no type)
|
||||
if ($nameIndex === null) {
|
||||
$nameIndex = 0;
|
||||
}
|
||||
|
||||
// Everything before nameIndex is the type
|
||||
if ($nameIndex > 0) {
|
||||
$type = \implode(' ', \array_slice($tokens, 0, $nameIndex));
|
||||
}
|
||||
|
||||
// Extract property name (strip $ if present)
|
||||
$name = \ltrim($tokens[$nameIndex], '$');
|
||||
|
||||
// Everything after the name is the description
|
||||
if ($nameIndex < \count($tokens) - 1) {
|
||||
$descParts = \array_slice($tokens, $nameIndex + 1);
|
||||
$description = \implode(' ', $descParts);
|
||||
}
|
||||
|
||||
return [
|
||||
'name' => $name,
|
||||
'type' => $type,
|
||||
'readOnly' => $tagName === 'property-read',
|
||||
'writeOnly' => $tagName === 'property-write',
|
||||
'description' => $description !== '' ? $description : null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse all @method tags from a raw docblock comment.
|
||||
*
|
||||
* @return ReflectionMagicMethod[]
|
||||
*/
|
||||
private static function parseMethodTags(string $docComment, \ReflectionClass $class): array
|
||||
{
|
||||
$methods = [];
|
||||
|
||||
if (\strpos($docComment, '@method') === false) {
|
||||
return $methods;
|
||||
}
|
||||
|
||||
// Match @method tags (handles multi-line by stopping at next @ or end)
|
||||
if (\preg_match_all('/@method\s+(.+?)(?=\n\s*\*\s*@|\n\s*\*\/|\z)/s', $docComment, $matches)) {
|
||||
foreach ($matches[1] as $body) {
|
||||
// Clean up the body (remove leading asterisks and normalize whitespace)
|
||||
$body = \preg_replace('/\n\s*\*\s*/', ' ', $body);
|
||||
$body = \trim($body);
|
||||
|
||||
$parsed = self::parseMethodTag($body);
|
||||
if ($parsed !== null) {
|
||||
$methods[] = new ReflectionMagicMethod(
|
||||
$class,
|
||||
$parsed['name'],
|
||||
$parsed['static'],
|
||||
$parsed['returnType'],
|
||||
$parsed['parameters'],
|
||||
$parsed['description'],
|
||||
$parsed['returnsReference']
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $methods;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse all @property tags from a raw docblock comment.
|
||||
*
|
||||
* @return ReflectionMagicProperty[]
|
||||
*/
|
||||
private static function parsePropertyTags(string $docComment, \ReflectionClass $class): array
|
||||
{
|
||||
$properties = [];
|
||||
|
||||
// Match @property, @property-read, @property-write tags
|
||||
$pattern = '/@(property(?:-read|-write)?)\s+(.+?)(?=\n\s*\*\s*@|\n\s*\*\/|\z)/s';
|
||||
|
||||
if (\preg_match_all($pattern, $docComment, $matches, \PREG_SET_ORDER)) {
|
||||
foreach ($matches as $match) {
|
||||
$tagName = $match[1];
|
||||
$body = \preg_replace('/\n\s*\*\s*/', ' ', $match[2]);
|
||||
$body = \trim($body);
|
||||
|
||||
$parsed = self::parsePropertyTag($body, $tagName);
|
||||
if ($parsed !== null) {
|
||||
$properties[] = new ReflectionMagicProperty(
|
||||
$class,
|
||||
$parsed['name'],
|
||||
$parsed['type'],
|
||||
$parsed['readOnly'],
|
||||
$parsed['writeOnly'],
|
||||
$parsed['description']
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $properties;
|
||||
}
|
||||
}
|
||||
|
||||
2
vendor/psy/psysh/src/Util/Json.php
vendored
2
vendor/psy/psysh/src/Util/Json.php
vendored
@@ -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.
|
||||
|
||||
2
vendor/psy/psysh/src/Util/Mirror.php
vendored
2
vendor/psy/psysh/src/Util/Mirror.php
vendored
@@ -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.
|
||||
|
||||
2
vendor/psy/psysh/src/Util/Str.php
vendored
2
vendor/psy/psysh/src/Util/Str.php
vendored
@@ -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.
|
||||
|
||||
356
vendor/psy/psysh/src/Util/TerminalColor.php
vendored
Normal file
356
vendor/psy/psysh/src/Util/TerminalColor.php
vendored
Normal file
@@ -0,0 +1,356 @@
|
||||
<?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\Util;
|
||||
|
||||
use Psy\Readline\Interactive\Helper\DebugLog;
|
||||
|
||||
/**
|
||||
* Terminal color detection and computation utility.
|
||||
*
|
||||
* Queries the terminal's background and foreground colors via OSC escape
|
||||
* sequences, then computes an appropriate input frame background by blending
|
||||
* a subtle tint over the detected color.
|
||||
*/
|
||||
class TerminalColor
|
||||
{
|
||||
/** @var array{bg: int[]|null, fg: int[]|null}|null */
|
||||
private static ?array $cachedColors = null;
|
||||
|
||||
private const OSC_FOREGROUND = '10';
|
||||
private const OSC_BACKGROUND = '11';
|
||||
private const QUERY_TIMEOUT_USEC = 50000; // 50ms
|
||||
|
||||
/**
|
||||
* Compute an appropriate input frame background color.
|
||||
*
|
||||
* Queries the terminal for its background color, then blends a subtle tint
|
||||
* on top: white at 12% for dark themes, black at 4% for light themes.
|
||||
*
|
||||
* Returns a hex color string (e.g. "#1a1a1a") or null if detection fails.
|
||||
*/
|
||||
public static function computeInputFrameBackground(): ?string
|
||||
{
|
||||
return self::blendOverTerminalBackground([255, 255, 255], 0.12, [0, 0, 0], 0.04);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute a red-tinted input frame background for syntax error feedback.
|
||||
*
|
||||
* Blends a subtle red tint over the terminal background: stronger for
|
||||
* dark themes (~15%), gentler for light themes (~8%).
|
||||
*/
|
||||
public static function computeInputFrameErrorBackground(): ?string
|
||||
{
|
||||
return self::blendOverTerminalBackground([200, 60, 60], 0.15, [200, 60, 60], 0.08);
|
||||
}
|
||||
|
||||
/**
|
||||
* Blend an overlay color over the detected terminal background.
|
||||
*
|
||||
* @param int[] $darkOverlay Overlay [r, g, b] for dark themes
|
||||
* @param float $darkAlpha Blend alpha for dark themes
|
||||
* @param int[] $lightOverlay Overlay [r, g, b] for light themes
|
||||
* @param float $lightAlpha Blend alpha for light themes
|
||||
*/
|
||||
private static function blendOverTerminalBackground(
|
||||
array $darkOverlay,
|
||||
float $darkAlpha,
|
||||
array $lightOverlay,
|
||||
float $lightAlpha
|
||||
): ?string {
|
||||
$colors = self::queryTerminalColors();
|
||||
if ($colors['bg'] === null) {
|
||||
DebugLog::log('TerminalColor', 'NO_BG_DETECTED');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$bg = $colors['bg'];
|
||||
|
||||
if (self::isLight($bg)) {
|
||||
$blended = self::blend($lightOverlay, $bg, $lightAlpha);
|
||||
} else {
|
||||
$blended = self::blend($darkOverlay, $bg, $darkAlpha);
|
||||
}
|
||||
|
||||
$hex = self::toHex($blended);
|
||||
|
||||
DebugLog::log('TerminalColor', 'INPUT_FRAME', [
|
||||
'theme' => self::isLight($bg) ? 'light' : 'dark',
|
||||
'bg' => self::toHex($bg),
|
||||
'blended' => $hex,
|
||||
]);
|
||||
|
||||
return $hex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Query the terminal's foreground and background colors.
|
||||
*
|
||||
* Results are cached for the lifetime of the process.
|
||||
*
|
||||
* @return array{bg: int[]|null, fg: int[]|null}
|
||||
*/
|
||||
public static function queryTerminalColors(): array
|
||||
{
|
||||
if (self::$cachedColors !== null) {
|
||||
return self::$cachedColors;
|
||||
}
|
||||
|
||||
self::$cachedColors = ['bg' => null, 'fg' => null];
|
||||
|
||||
$tty = self::openTty();
|
||||
if ($tty === null) {
|
||||
DebugLog::log('TerminalColor', 'SKIP', ['reason' => 'no_tty']);
|
||||
|
||||
return self::$cachedColors;
|
||||
}
|
||||
|
||||
$startTime = \microtime(true);
|
||||
|
||||
try {
|
||||
$sttyState = self::saveStty();
|
||||
if ($sttyState === null) {
|
||||
DebugLog::log('TerminalColor', 'SKIP', ['reason' => 'stty_failed']);
|
||||
|
||||
return self::$cachedColors;
|
||||
}
|
||||
|
||||
try {
|
||||
// Put terminal in raw mode for direct I/O
|
||||
@\shell_exec('stty -echo -icanon min 0 time 0 < /dev/tty 2>/dev/null');
|
||||
|
||||
// Query background and foreground together
|
||||
\fwrite($tty, "\033]".self::OSC_BACKGROUND.";?\033\\\033]".self::OSC_FOREGROUND.";?\033\\");
|
||||
\fflush($tty);
|
||||
|
||||
$response = self::readResponse($tty);
|
||||
|
||||
$elapsed = (\microtime(true) - $startTime) * 1000;
|
||||
|
||||
if ($response !== '') {
|
||||
self::$cachedColors['bg'] = self::parseOscResponse($response, self::OSC_BACKGROUND);
|
||||
self::$cachedColors['fg'] = self::parseOscResponse($response, self::OSC_FOREGROUND);
|
||||
|
||||
DebugLog::log('TerminalColor', 'OSC_QUERY', [
|
||||
'elapsed_ms' => \round($elapsed, 1),
|
||||
'bg' => self::$cachedColors['bg'] !== null ? self::toHex(self::$cachedColors['bg']) : 'null',
|
||||
'fg' => self::$cachedColors['fg'] !== null ? self::toHex(self::$cachedColors['fg']) : 'null',
|
||||
'response' => $response,
|
||||
]);
|
||||
} else {
|
||||
DebugLog::log('TerminalColor', 'OSC_QUERY', [
|
||||
'elapsed_ms' => \round($elapsed, 1),
|
||||
'result' => 'no_response',
|
||||
]);
|
||||
}
|
||||
} finally {
|
||||
self::restoreStty($sttyState);
|
||||
}
|
||||
} finally {
|
||||
@\fclose($tty);
|
||||
}
|
||||
|
||||
return self::$cachedColors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether an RGB color is light (high luminance).
|
||||
*
|
||||
* @param int[] $rgb [r, g, b] values 0-255
|
||||
*/
|
||||
public static function isLight(array $rgb): bool
|
||||
{
|
||||
return self::luminance($rgb) > 128;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute perceived luminance for an RGB color.
|
||||
*
|
||||
* Uses the ITU-R BT.601 luma formula.
|
||||
*
|
||||
* @param int[] $rgb [r, g, b] values 0-255
|
||||
*/
|
||||
public static function luminance(array $rgb): float
|
||||
{
|
||||
return 0.299 * $rgb[0] + 0.587 * $rgb[1] + 0.114 * $rgb[2];
|
||||
}
|
||||
|
||||
/**
|
||||
* Alpha-composite an overlay color onto a base color.
|
||||
*
|
||||
* @param int[] $overlay [r, g, b] values 0-255
|
||||
* @param int[] $base [r, g, b] values 0-255
|
||||
*
|
||||
* @return int[] [r, g, b] blended result
|
||||
*/
|
||||
public static function blend(array $overlay, array $base, float $alpha): array
|
||||
{
|
||||
return [
|
||||
(int) \round($overlay[0] * $alpha + $base[0] * (1 - $alpha)),
|
||||
(int) \round($overlay[1] * $alpha + $base[1] * (1 - $alpha)),
|
||||
(int) \round($overlay[2] * $alpha + $base[2] * (1 - $alpha)),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an RGB array to a hex color string.
|
||||
*
|
||||
* @param int[] $rgb [r, g, b] values 0-255
|
||||
*/
|
||||
public static function toHex(array $rgb): string
|
||||
{
|
||||
return \sprintf('#%02x%02x%02x', $rgb[0], $rgb[1], $rgb[2]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse an OSC color response for a given parameter number.
|
||||
*
|
||||
* Handles the `rgb:R/G/B`, `rgb:RR/GG/BB`, `rgb:RRR/GGG/BBB`, and
|
||||
* `rgb:RRRR/GGGG/BBBB` formats returned by xterm-compatible terminals.
|
||||
*
|
||||
* @return int[]|null [r, g, b] values 0-255, or null on parse failure
|
||||
*/
|
||||
public static function parseOscResponse(string $response, string $param): ?array
|
||||
{
|
||||
// Match OSC response: ESC ] <param> ; rgb:R.../G.../B... (terminated by BEL or ST)
|
||||
$pattern = '/\033\]'.$param.';rgb:([0-9a-fA-F]{1,4})\/([0-9a-fA-F]{1,4})\/([0-9a-fA-F]{1,4})/';
|
||||
|
||||
if (!\preg_match($pattern, $response, $matches)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Components are 1-4 hex digits; normalize each channel to 0-255.
|
||||
return [
|
||||
self::scaleColorComponent($matches[1]),
|
||||
self::scaleColorComponent($matches[2]),
|
||||
self::scaleColorComponent($matches[3]),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Scale a hex color component string to an 8-bit value (0-255).
|
||||
*
|
||||
* Components are normalized by bit depth:
|
||||
* - 1 hex digit (4-bit) 0x0-0xf -> 0-255
|
||||
* - 2 hex digits (8-bit) 0x00-0xff -> 0-255
|
||||
* - 3 hex digits (12-bit) 0x000-0xfff -> 0-255
|
||||
* - 4 hex digits (16-bit) 0x0000-0xffff -> 0-255
|
||||
*/
|
||||
private static function scaleColorComponent(string $hex): int
|
||||
{
|
||||
$digits = \strlen($hex);
|
||||
$value = \hexdec($hex);
|
||||
|
||||
// 8-bit values are already in the target range.
|
||||
if ($digits === 2) {
|
||||
return (int) $value;
|
||||
}
|
||||
|
||||
$maxValue = (16 ** $digits) - 1;
|
||||
|
||||
return (int) \round(($value * 255) / $maxValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Open /dev/tty for terminal I/O.
|
||||
*
|
||||
* @return resource|null
|
||||
*/
|
||||
private static function openTty()
|
||||
{
|
||||
$tty = @\fopen('/dev/tty', 'r+');
|
||||
if ($tty === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
\stream_set_blocking($tty, false);
|
||||
|
||||
return $tty;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the current stty state.
|
||||
*/
|
||||
private static function saveStty(): ?string
|
||||
{
|
||||
$state = @\shell_exec('stty -g < /dev/tty 2>/dev/null');
|
||||
if (!\is_string($state) || \trim($state) === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return \trim($state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore a previously saved stty state.
|
||||
*/
|
||||
private static function restoreStty(string $state): void
|
||||
{
|
||||
@\shell_exec(\sprintf('stty %s < /dev/tty 2>/dev/null', \escapeshellarg($state)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the terminal's response with a short timeout.
|
||||
*
|
||||
* @param resource $tty
|
||||
*/
|
||||
private static function readResponse($tty): string
|
||||
{
|
||||
$response = '';
|
||||
$deadline = \microtime(true) + self::QUERY_TIMEOUT_USEC / 1000000;
|
||||
|
||||
while (\microtime(true) < $deadline) {
|
||||
$read = [$tty];
|
||||
$write = null;
|
||||
$except = null;
|
||||
$remainingUsec = (int) (($deadline - \microtime(true)) * 1000000);
|
||||
if ($remainingUsec <= 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
$ready = @\stream_select($read, $write, $except, 0, $remainingUsec);
|
||||
if ($ready === false || $ready === 0) {
|
||||
// No data yet; if we already have some response, we're probably done
|
||||
if ($response !== '') {
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
$chunk = @\fread($tty, 256);
|
||||
if ($chunk === false || $chunk === '') {
|
||||
break;
|
||||
}
|
||||
|
||||
$response .= $chunk;
|
||||
|
||||
// Stop if we've received both responses (two terminators, ST or BEL)
|
||||
$terminators = \substr_count($response, "\033\\") + \substr_count($response, "\x07");
|
||||
if ($terminators >= 2) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the cached terminal colors.
|
||||
*
|
||||
* Primarily for testing.
|
||||
*/
|
||||
public static function resetCache(): void
|
||||
{
|
||||
self::$cachedColors = null;
|
||||
}
|
||||
}
|
||||
102
vendor/psy/psysh/src/Util/Tty.php
vendored
Normal file
102
vendor/psy/psysh/src/Util/Tty.php
vendored
Normal file
@@ -0,0 +1,102 @@
|
||||
<?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\Util;
|
||||
|
||||
/**
|
||||
* TTY detection utility.
|
||||
*/
|
||||
class Tty
|
||||
{
|
||||
private static ?bool $sttySupported = null;
|
||||
private const DEFAULT_WIDTH = 100;
|
||||
|
||||
/**
|
||||
* Check whether stty is available for terminal manipulation.
|
||||
*
|
||||
* Verifies that we're on a Unix-like system with shell_exec and a TTY
|
||||
* on stdin, and that stty actually works.
|
||||
*/
|
||||
public static function supportsStty(): bool
|
||||
{
|
||||
if (self::$sttySupported !== null) {
|
||||
return (bool) self::$sttySupported;
|
||||
}
|
||||
|
||||
if (\PHP_OS_FAMILY === 'Windows' || !\function_exists('shell_exec')) {
|
||||
return self::$sttySupported = false;
|
||||
}
|
||||
|
||||
if (!\defined('STDIN') || !self::isatty(\STDIN)) {
|
||||
return self::$sttySupported = false;
|
||||
}
|
||||
|
||||
$stty = @\shell_exec('stty -g 2>/dev/null');
|
||||
|
||||
return self::$sttySupported = \is_string($stty) && \trim($stty) !== '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a stream is a TTY.
|
||||
*
|
||||
* Falls back gracefully when stream_isatty and posix_isatty are
|
||||
* unavailable, using fstat to check for a character device.
|
||||
*
|
||||
* Returns false when detection is uncertain.
|
||||
*
|
||||
* @param resource|int $stream
|
||||
*/
|
||||
public static function isatty($stream): bool
|
||||
{
|
||||
if (\function_exists('stream_isatty')) {
|
||||
return @\stream_isatty($stream);
|
||||
}
|
||||
|
||||
if (\function_exists('posix_isatty')) {
|
||||
return @\posix_isatty($stream);
|
||||
}
|
||||
|
||||
// Fallback: check fstat mode for character device (TTY = 0020000)
|
||||
$stat = @\fstat($stream);
|
||||
if (!\is_array($stat) || !isset($stat['mode'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ($stat['mode'] & 0170000) === 0020000;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current terminal width in columns.
|
||||
*/
|
||||
public static function getWidth(int $default = self::DEFAULT_WIDTH): int
|
||||
{
|
||||
if (self::supportsStty() && \defined('STDOUT') && self::isatty(\STDOUT)) {
|
||||
// Output format: "rows cols"
|
||||
$size = @\shell_exec('stty size </dev/tty 2>/dev/null');
|
||||
if ($size && \preg_match('/^\d+ (\d+)$/', \trim($size), $matches)) {
|
||||
return (int) $matches[1];
|
||||
}
|
||||
|
||||
$width = @\shell_exec('tput cols </dev/tty 2>/dev/null');
|
||||
if ($width && \is_numeric(\trim($width))) {
|
||||
return (int) \trim($width);
|
||||
}
|
||||
}
|
||||
|
||||
// Check COLUMNS environment variable (may be stale after resize)
|
||||
$width = \getenv('COLUMNS');
|
||||
if ($width && \is_numeric(\trim($width))) {
|
||||
return (int) \trim($width);
|
||||
}
|
||||
|
||||
return $default;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user