First Commit
This commit is contained in:
+223
@@ -0,0 +1,223 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\Helper;
|
||||
|
||||
use Symfony\Component\Console\Exception\InvalidFileException;
|
||||
use Symfony\Component\Console\Exception\MissingInputException;
|
||||
use Symfony\Component\Console\Formatter\OutputFormatter;
|
||||
use Symfony\Component\Console\Input\File\InputFile;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Question\FileQuestion;
|
||||
use Symfony\Component\Console\Terminal;
|
||||
use Symfony\Component\Console\Terminal\Image\ImageProtocolInterface;
|
||||
use Symfony\Component\Console\Terminal\Image\ITerm2Protocol;
|
||||
use Symfony\Component\Console\Terminal\Image\KittyGraphicsProtocol;
|
||||
|
||||
/**
|
||||
* Orchestrates file input handling through paste detection or path input.
|
||||
*
|
||||
* @author Robin Chalas <robin.chalas@gmail.com>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class FileInputHelper
|
||||
{
|
||||
private const BPM_ENABLE = "\x1b[?2004h";
|
||||
private const BPM_DISABLE = "\x1b[?2004l";
|
||||
private const PASTE_START = "\x1b[200~";
|
||||
private const PASTE_END = "\x1b[201~";
|
||||
private const MAX_PASTE_BYTES = 16 * 1024 * 1024;
|
||||
|
||||
private ?ImageProtocolInterface $protocol = null;
|
||||
|
||||
/**
|
||||
* @param resource $inputStream
|
||||
*/
|
||||
public function readFileInput($inputStream, OutputInterface $output, FileQuestion $question): InputFile
|
||||
{
|
||||
if ($canPaste = $question->isPasteAllowed() && Terminal::supportsImageProtocol() && Terminal::hasSttyAvailable()) {
|
||||
$this->protocol = $this->detectProtocol();
|
||||
}
|
||||
|
||||
$file = null;
|
||||
$inputHelper = null;
|
||||
|
||||
try {
|
||||
if ($canPaste) {
|
||||
$inputHelper = new TerminalInputHelper($inputStream);
|
||||
$output->write(self::BPM_ENABLE);
|
||||
shell_exec('stty -icanon -echo');
|
||||
|
||||
$file = $this->readWithPasteDetection($inputStream, $output, $question, $inputHelper);
|
||||
} elseif ($question->isPathAllowed()) {
|
||||
$file = $this->readPathInput($inputStream);
|
||||
} else {
|
||||
throw new MissingInputException('Terminal does not support image paste and path input is disabled.');
|
||||
}
|
||||
} finally {
|
||||
if ($canPaste) {
|
||||
$output->write(self::BPM_DISABLE);
|
||||
$inputHelper?->finish();
|
||||
}
|
||||
}
|
||||
|
||||
if (!$file->isValid()) {
|
||||
throw new InvalidFileException(\sprintf('File "%s" is not valid or readable.', $file->getPathname()));
|
||||
}
|
||||
|
||||
$this->displayFile($output, $file);
|
||||
|
||||
return $file;
|
||||
}
|
||||
|
||||
public function displayFile(OutputInterface $output, InputFile $file): void
|
||||
{
|
||||
$link = \sprintf('<href=file://%s>%s</>', OutputFormatter::escape($file->getRealPath()), OutputFormatter::escape($file->getFilename()));
|
||||
|
||||
if ($output->isVeryVerbose()) {
|
||||
$output->writeln(\sprintf('<info>%s</info> %s (<comment>%s, %s</comment>)', "\u{1F4CE}", $link, OutputFormatter::escape($file->getMimeType() ?? 'unknown'), $file->getHumanReadableSize()));
|
||||
} else {
|
||||
$output->writeln(\sprintf('<info>%s</info> %s', "\u{1F4CE}", $link));
|
||||
}
|
||||
|
||||
if (Terminal::supportsImageProtocol() && $this->isDisplayableImage($file)) {
|
||||
$this->displayThumbnail($output, $file);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param resource $inputStream
|
||||
*/
|
||||
private function readWithPasteDetection($inputStream, OutputInterface $output, FileQuestion $question, TerminalInputHelper $inputHelper): InputFile
|
||||
{
|
||||
$buffer = '';
|
||||
$inPaste = false;
|
||||
$pasteBuffer = '';
|
||||
|
||||
while (!feof($inputStream)) {
|
||||
$inputHelper->waitForInput();
|
||||
$char = fread($inputStream, 1);
|
||||
|
||||
if (false === $char || '' === $char) {
|
||||
if ('' === $buffer && '' === $pasteBuffer) {
|
||||
throw new MissingInputException('Aborted.');
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
$buffer .= $char;
|
||||
|
||||
if (\strlen($buffer) > self::MAX_PASTE_BYTES) {
|
||||
throw new InvalidFileException(\sprintf('Pasted input exceeds the maximum allowed size of %d bytes.', self::MAX_PASTE_BYTES));
|
||||
}
|
||||
|
||||
if (!$inPaste && str_ends_with($buffer, self::PASTE_START)) {
|
||||
$inPaste = true;
|
||||
$buffer = substr($buffer, 0, -\strlen(self::PASTE_START));
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($inPaste && str_ends_with($buffer, self::PASTE_END)) {
|
||||
$pasteBuffer = substr($buffer, 0, -\strlen(self::PASTE_END));
|
||||
break;
|
||||
}
|
||||
|
||||
if (!$inPaste && ("\n" === $char || "\r" === $char)) {
|
||||
$buffer = rtrim($buffer, "\r\n");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ('' !== $pasteBuffer) {
|
||||
if (null !== $this->protocol && $this->protocol->detectPastedImage($pasteBuffer)) {
|
||||
$decoded = $this->protocol->decode($pasteBuffer);
|
||||
if ('' !== $decoded['data']) {
|
||||
return InputFile::fromData($decoded['data'], $decoded['format']);
|
||||
}
|
||||
}
|
||||
|
||||
$path = trim($pasteBuffer);
|
||||
if ('' !== $path && $question->isPathAllowed()) {
|
||||
return InputFile::fromPath($path);
|
||||
}
|
||||
}
|
||||
|
||||
$path = trim($buffer);
|
||||
if ('' !== $path && $question->isPathAllowed()) {
|
||||
return InputFile::fromPath($path);
|
||||
}
|
||||
|
||||
throw new MissingInputException('No file input provided.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param resource $inputStream
|
||||
*/
|
||||
private function readPathInput($inputStream): InputFile
|
||||
{
|
||||
if (!$isBlocked = stream_get_meta_data($inputStream)['blocked'] ?? true) {
|
||||
stream_set_blocking($inputStream, true);
|
||||
}
|
||||
|
||||
$path = fgets($inputStream);
|
||||
|
||||
if (!$isBlocked) {
|
||||
stream_set_blocking($inputStream, false);
|
||||
}
|
||||
|
||||
if (false === $path) {
|
||||
throw new MissingInputException('Aborted.');
|
||||
}
|
||||
|
||||
if ('' === $path = trim($path)) {
|
||||
throw new MissingInputException('No file path provided.');
|
||||
}
|
||||
|
||||
return InputFile::fromPath($path);
|
||||
}
|
||||
|
||||
private function detectProtocol(): ?ImageProtocolInterface
|
||||
{
|
||||
if (Terminal::supportsKittyGraphics()) {
|
||||
return new KittyGraphicsProtocol();
|
||||
}
|
||||
|
||||
if (Terminal::supportsITerm2Images()) {
|
||||
return new ITerm2Protocol();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function isDisplayableImage(InputFile $file): bool
|
||||
{
|
||||
if (null === $mimeType = $file->getMimeType()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return str_starts_with($mimeType, 'image/');
|
||||
}
|
||||
|
||||
private function displayThumbnail(OutputInterface $output, InputFile $file): void
|
||||
{
|
||||
try {
|
||||
$contents = $file->getContents();
|
||||
} catch (InvalidFileException) {
|
||||
return;
|
||||
}
|
||||
|
||||
$protocol = Terminal::supportsKittyGraphics() ? new KittyGraphicsProtocol() : new ITerm2Protocol();
|
||||
|
||||
$output->write($protocol->encode($contents, 16));
|
||||
$output->writeln('');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user