diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..3b20d51 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,55 @@ +name: Bug Report +description: Report an issue with ConsoleColor +title: "[Bug]: " +labels: ["bug"] +body: + - type: markdown + attributes: + value: Thank you for taking the time to fill out this bug report! + + - type: textarea + attributes: + label: Description + description: A brief description of the bug + validations: + required: true + + - type: textarea + attributes: + label: Steps to Reproduce + description: What was the process that led to this bug? + value: | + 1. + 2. + 3. + ... + validations: + required: true + + - type: textarea + attributes: + label: Expected Result + description: What did you expect to happen when following the steps to reproduce? + validations: + required: true + + - type: textarea + attributes: + label: Actual Result + description: What actually happened? + validations: + required: true + + - type: input + attributes: + label: ConsoleColor Version + placeholder: "1.2.3" + validations: + required: true + + - type: input + attributes: + label: PHP Version + placeholder: "8.x.x" + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..d7453f5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,34 @@ +name: Feature Request +description: Propose a new feature for ConsoleColor +title: "[Feature]: " +labels: ["feature"] +body: + - type: markdown + attributes: + value: Thank you for taking the time to propose a new feature! + + - type: textarea + attributes: + label: Description + description: A brief description of the feature you'd like to propose + validations: + required: true + + - type: textarea + attributes: + label: Example Code + description: | + One or more examples of how you think the feature should work and what + its results would be + render: php + + - type: dropdown + attributes: + label: Release Target + description: | + Would this feature be a major (breaking) change to existing + functionality or can it go into a minor release? + options: + - Unknown + - Minor + - Major diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..62426a3 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,40 @@ + + + + + + +## Description + + + +## Contribution Checklist + +- [ ] The contents of this pull request are my own work and may be distributed under the terms of the project license +- [ ] I have read and agree to the [Contributing](CONTRIBUTING.md) guidelines and the [Code of Conduct](CODE_OF_CONDUCT.md) +- [ ] All new changes are covered by appropriate tests +- [ ] All changes pass code style checks and static analysis +- [ ] All previous tests and checks are passing +- [ ] I have included documentation about this change +- [ ] The details of this change have been added to the `Unreleased` section of the [changelog](CHANGELOG.md) + +**This pull request includes:** + +- [ ] Breaking changes to existing functionality (major release) +- [ ] New functionality (minor release) +- [ ] Fixes for existing functionality (patch release) +- [ ] Fixes to a previous major or minor release version diff --git a/.github/workflows/acceptance.yml b/.github/workflows/acceptance.yml index 2902379..ecbaa62 100644 --- a/.github/workflows/acceptance.yml +++ b/.github/workflows/acceptance.yml @@ -23,10 +23,10 @@ jobs: - php: "8.2" - php: "8.3" - php: "8.4" - # - php: "8.5" + - php: "8.5" steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 - name: Setup PHP ${{ matrix.versions.php }} uses: shivammathur/setup-php@v2 with: @@ -38,7 +38,7 @@ jobs: run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: Cache Dependencies - uses: actions/cache@v3 + uses: actions/cache@v5 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ matrix.versions.php }}-${{ hashFiles('**/composer.json') }} @@ -49,6 +49,9 @@ jobs: - name: Install Dependencies run: composer install --no-interaction --prefer-dist --no-progress + - name: Check composer.json Format + run: composer normalize --diff --dry-run --no-interaction --ansi + - name: Check Code Style run: composer style:check diff --git a/README.md b/README.md index 9303d3c..89bf91c 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ + # Console Color [![Latest Stable Version](https://img.shields.io/packagist/v/bebat/console-color.svg?style=flat-square)](https://packagist.org/packages/bebat/console-color) @@ -11,12 +12,13 @@ Console Color is a lightweight PHP 8.1+ library for adding color & other styles - [Installation](#installation) - [Basic Usage](#basic-usage) - [Environment Variables](#environment-variables) + - [Auto Termination](#auto-termination) - [Included Styles](#included-styles) - [Basic Styles](#basic-styles) - [Text](#text) - [Underline](#underline) - - [Foreground & Background Color](#foreground--background-color) - - [256 & True Color](#256--true-color) + - [Foreground \& Background Color](#foreground--background-color) + - [256 \& True Color](#256--true-color) - [Composite Styles](#composite-styles) ## Installation @@ -77,6 +79,72 @@ In addition to checking if `STDOUT` is a TTY, `Style` will look at several envir * `TERM` - `Style` will check `TERM` to see if it supports 256 colors. * `COLORTERM` - If `COLORTERM` is set to `truecolor` then `Style` will apply RGB based colors. +### Auto Termination + +By default, Console Color will "terminate" each style by appending `Style\Text::None` after the text you are applying styles to. This is helpful so you don't accidentally make all the text in the terminal bright red, for example. However, if you are outputting many styles to the screen and would like more control on when they are terminated this can be disabled globally or at call time. + +To disable termination globally, pass `false` to `autoTerminate()` like so: + +```php +use BeBat\ConsoleColor\Style; + +$style = new Style(); +$style->autoTerminate(false); + +echo $style->apply("Didn't I just warn you about this?\n", Style\Color::BrightRed); +``` + +Auto termination can be re-enabled simply by calling `autoTerminate()` as well. + +Auto termination can also be disabled at call time by passing `false` as the third parameter to `apply()`: + +```php +use BeBat\ConsoleColor\Style; + +$style = new Style(); + +echo $style->apply("This style wont't stop!\n", Style\Color::Yellow, false); +echo $style->apply("Now we're mixing and matching styles?\n", Style\BackgroundColor::BrightRed, false); +echo $style->apply("Let's stop things before they get too out of hand\n", Style\Text::Underline, true); +``` + +The `Style` instance keeps track of whether the previous style was terminated or not. You can use `willAutoTerminate()` and `isActive()` to determine whether auto termination is enabled and if there are styles currently active on the output stream. + +To manually end styling, you can output `terminate()`: + +```php +use BeBat\ConsoleColor\Style; + +$style = new Style(); +$style->autoTerminate(false); + +try { + echo $style->apply("We're about to attempt something that could fail!\n", Style\BackgroundColor::BrightYellow); + + // ... +} catch (\Throwable $e) { + if ($style->isActive()) { + // Previously applied style(s) weren't stopped + echo $style->terminate(); + } +} +``` + +Lastly, the `apply()` method will also check to see if styles were terminated by being passed `Style\Text::None`: + +```php +use BeBat\ConsoleColor\Style; + +$style = new Style(); +$style->autoTerminate(false); + +echo $style->apply("Let's keep styling forever!\n", Style\Color::Blue); +$style->isActive(); // => true + +echo $style->apply("Never mind, I've grown bored of such things.\n", Style\Text::None); +$style->isActive(); // => false +``` + ## Included Styles `Style::apply()` accepts an instance of [`StyleInterface`](src/StyleInterface.php), and Console Color includes a number of styles that implement this interface. @@ -174,7 +242,7 @@ echo $style->apply( ) . PHP_EOL; ``` -`Style\Composite()` can actually take as many styles as you need to apply: +`Style\Composite()` can take as many styles as you need to apply at once: ```php use BeBat\ConsoleColor\Style; diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..42a2586 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,3 @@ +# Security Policy + +Because of their sensitive nature, security issues should **not** be discussed in public forums. If you believe you have found an issue that exposes sensitive information or that could be leveraged in accessing such information, please email it directly to the project maintainer at . We will respond promptly and do our utmost to resolve any security issues in a timely manner. diff --git a/captainhook.json b/captainhook.json index 0c101ef..449effa 100644 --- a/captainhook.json +++ b/captainhook.json @@ -18,7 +18,7 @@ "conditions": [] }, { - "action": "composer test:phpunit -- --colors=never", + "action": "vendor/bin/phpunit --colors=never", "options": [], "conditions": [] } diff --git a/composer.json b/composer.json index de66653..4cfd9e1 100644 --- a/composer.json +++ b/composer.json @@ -28,16 +28,16 @@ "bebat/verify": "~3.2.0", "captainhook/captainhook": "~5.25.11", "captainhook/plugin-composer": "~5.3.3", - "ergebnis/composer-normalize": "~2.33", - "friendsofphp/php-cs-fixer": "^3.89.2", - "maglnet/composer-require-checker": "~4.6", + "ergebnis/composer-normalize": "~2.48.2", + "friendsofphp/php-cs-fixer": "~3.91.3", + "maglnet/composer-require-checker": "^4.6", "mockery/mockery": "~1.6.2", "phpstan/extension-installer": "~1.4.3", - "phpstan/phpstan": "~1.12.32", - "phpstan/phpstan-deprecation-rules": "~1.2.1", - "phpstan/phpstan-mockery": "~1.1.1", - "phpstan/phpstan-phpunit": "~1.4.2", - "phpstan/phpstan-strict-rules": "~1.6.2", + "phpstan/phpstan": "~2.1.32", + "phpstan/phpstan-deprecation-rules": "~2.0.3", + "phpstan/phpstan-mockery": "~2.0.0", + "phpstan/phpstan-phpunit": "~2.0.8", + "phpstan/phpstan-strict-rules": "~2.0.7", "phpunit/phpunit": "~10.5.58", "zalas/phpunit-globals": "~4.0.1" }, @@ -74,7 +74,7 @@ "@test:phpunit" ], "test:coverage": "phpunit --coverage-clover=coverage.xml", - "test:phpunit": "phpunit", + "test:phpunit": "phpunit --colors=always", "test:static": "phpstan analyze --ansi" }, "scripts-descriptions": { diff --git a/phpstan.neon.dist b/phpstan.neon.dist index fd24ba2..9309fdb 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -3,8 +3,6 @@ parameters: paths: - src - test - checkMissingIterableValueType: true ignoreErrors: - - '/Readonly property BeBat\\ConsoleColor\\Style::.+ is assigned outside of the constructor/' - '/Call to an undefined method BeBat\\Verify\\API\\Value::/' - '/Access to an undefined property BeBat\\Verify\\API\\Value::/' diff --git a/sample.php b/sample.php index b72f2e1..ad99c24 100755 --- a/sample.php +++ b/sample.php @@ -3,10 +3,9 @@ declare(strict_types=1); +use BeBat\ConsoleColor\ApplierInterface; use BeBat\ConsoleColor\Style; - -use const PHP_EOL; -use const STR_PAD_LEFT; +use BeBat\ConsoleColor\StyleInterface; $localPath = __DIR__ . '/vendor/autoload.php'; $installedPath = dirname(__DIR__, 2) . '/autoload.php'; @@ -19,138 +18,128 @@ throw new RuntimeException('Could not find autoload file. Make sure to run "composer install" first.'); } -$style = new Style(); -$style->force(); - -echo 'Styling supported: ' . ($style->supportsStyles ? 'Yes' : 'No') . PHP_EOL; -echo '256 colors supported: ' . ($style->supports256Colors ? 'Yes' : 'No') . PHP_EOL; -echo '24-bit true colors supported: ' . ($style->supportsRGBColors ? 'Yes' : 'No') . PHP_EOL; - -echo PHP_EOL; - -function printEnumStyleSample(string $heading, string $enum): void +final class StyleDemo { - global $style; - - echo $heading . PHP_EOL; - - foreach ($enum::cases() as $styleValue) { - echo ' ' . $style->apply($styleValue->name, $styleValue) . PHP_EOL; - } - - echo PHP_EOL; -} - -function print256ColorSample(string $heading, string $method): void -{ - global $style; - - echo "256 {$heading} Colors" . PHP_EOL; - - for ($i = 0; $i < 256; ++$i) { - if ($i === 0 || $i < 231 && $i % 18 === 16 || 231 < $i && $i % 12 === 4) { - echo ' '; - } - - echo $style->apply( - str_pad((string) $i, 4, ' ', STR_PAD_LEFT), - Style\Color256::{$method}($i), + private const INDENT = ' '; + + public function __construct(private ApplierInterface $applier) {} + + /** + * @param class-string $enum + */ + public function enumStyle(string $enum): string + { + return array_reduce( + $enum::cases(), + fn (string $result, StyleInterface|UnitEnum $style): string => $result . self::INDENT . $this->applier->apply($style->name, $style) . \PHP_EOL, + '', ); - - if ($i === 15 || $i === 231) { - echo PHP_EOL . PHP_EOL; - } - if (15 < $i && $i < 231 && $i % 18 === 15 || $i === 243 || $i === 255) { - echo PHP_EOL; - } } - echo PHP_EOL; -} + /** + * @param 'background'|'foreground'|'underline' $method + */ + public function style256Color(string $method): string + { + $result = ''; -function printTrueColorSample(string $heading, string $method): void -{ - global $style; + $this->applier->autoTerminate(false); - echo "True Color {$heading} (Abridged)" . PHP_EOL; + for ($i = 0; $i < 256; ++$i) { + $style = Style\Color256::{$method}($i); - for ($red = 0; $red < 8; ++$red) { - for ($green = 0; $green < 8; ++$green) { - for ($blue = 0; $blue < 8; ++$blue) { - if ($blue % 8 === 0 && $green % 8 === 0) { - echo ' '; - } - - echo $style->apply( - 'X', - Style\ColorRGB::{$method}($red * 32, $green * 32, $blue * 32), - ); - - if ($blue % 8 === 7 && $green % 8 === 7) { - echo PHP_EOL; - } + if ($method === 'underline') { + $style = new Style\Composite(Style\Text::Underline, $style); } - } - } - echo PHP_EOL; -} + if ($i === 0 || $i < 231 && $i % 18 === 16 || 231 < $i && $i % 12 === 4) { + $result .= self::INDENT; + } -printEnumStyleSample('Text Styles', Style\Text::class); -printEnumStyleSample('Underline Styles', Style\Underline::class); -printEnumStyleSample('Foreground Colors', Style\Color::class); -printEnumStyleSample('Background Colors', Style\BackgroundColor::class); + $result .= $this->applier->apply( + str_pad((string) $i, 4, ' ', \STR_PAD_LEFT), + $style, + ); -print256ColorSample('Foreground', 'foreground'); -print256ColorSample('Background', 'background'); + if ($i === 15 || $i === 231) { + $result .= $this->applier->terminate() . \PHP_EOL . \PHP_EOL; + } + if (15 < $i && $i < 231 && $i % 18 === 15 || $i === 243 || $i === 255) { + $result .= $this->applier->terminate() . \PHP_EOL; + } + } -echo '256 Underline Colors' . PHP_EOL; + $this->applier->autoTerminate(); -for ($i = 0; $i < 256; ++$i) { - if ($i === 0 || $i < 231 && $i % 18 === 16 || 231 < $i && $i % 12 === 4) { - echo ' '; + return $result; } - echo $style->apply( - str_pad((string) $i, 4, ' ', STR_PAD_LEFT), - new Style\Composite(Style\Text::Underline, Style\Color256::underline($i)), - ); + /** + * @param 'background'|'foreground'|'underline' $method + */ + public function styleTrueColor(string $method): string + { + $result = ''; - if ($i === 15 || $i === 231) { - echo PHP_EOL . PHP_EOL; - } - if (15 < $i && $i < 231 && $i % 18 === 15 || $i === 243 || $i === 255) { - echo PHP_EOL; - } -} + $this->applier->autoTerminate(false); -echo PHP_EOL; + for ($red = 0; $red < 8; ++$red) { + for ($green = 0; $green < 8; ++$green) { + for ($blue = 0; $blue < 8; ++$blue) { + $style = Style\ColorRGB::{$method}($red * 32, $green * 32, $blue * 32); -printTrueColorSample('Foreground', 'foreground'); -printTrueColorSample('Background', 'background'); + if ($method === 'underline') { + $style = new Style\Composite(Style\Text::Underline, $style); + } -echo 'True Color Underline (Abridged)' . PHP_EOL; + if ($blue % 8 === 0 && $green % 8 === 0) { + $result .= self::INDENT; + } -for ($red = 0; $red < 8; ++$red) { - for ($green = 0; $green < 8; ++$green) { - for ($blue = 0; $blue < 8; ++$blue) { - if ($blue % 8 === 0 && $green % 8 === 0) { - echo ' '; - } - - echo $style->apply( - 'X', - new Style\Composite( - Style\Text::Underline, - Style\ColorRGB::underline($red * 32, $green * 32, $blue * 32), - ), - ); + $result .= $this->applier->apply('X', $style); - if ($blue % 8 === 7 && $green % 8 === 7) { - echo PHP_EOL; + if ($blue % 8 === 7 && $green % 8 === 7) { + $result .= $this->applier->terminate() . \PHP_EOL; + } + } } } + + $this->applier->autoTerminate(); + + return $result; } } -echo PHP_EOL; +$style = new Style(); +$style->force(); +$demo = new StyleDemo($style); + +echo 'Styling supported: ' . ($style->supportsStyles ? 'Yes' : 'No') . \PHP_EOL; +echo '256 colors supported: ' . ($style->supports256Colors ? 'Yes' : 'No') . \PHP_EOL; +echo '24-bit true colors supported: ' . ($style->supportsRGBColors ? 'Yes' : 'No') . \PHP_EOL; + +echo \PHP_EOL; + +echo 'Text Styles' . \PHP_EOL; +echo $demo->enumStyle(Style\Text::class) . \PHP_EOL; +echo 'Underline Styles' . \PHP_EOL; +echo $demo->enumStyle(Style\Underline::class) . \PHP_EOL; +echo 'Foreground Colors' . \PHP_EOL; +echo $demo->enumStyle(Style\Color::class) . \PHP_EOL; +echo 'Background Colors' . \PHP_EOL; +echo $demo->enumStyle(Style\BackgroundColor::class) . \PHP_EOL; + +echo \PHP_EOL . '256 Foreground Colors' . \PHP_EOL; +echo $demo->style256Color('foreground'); +echo \PHP_EOL . '256 Background Colors' . \PHP_EOL; +echo $demo->style256Color('background'); +echo \PHP_EOL . '256 Underline Colors' . \PHP_EOL; +echo $demo->style256Color('underline'); + +echo \PHP_EOL . 'True Color Foreground (Abridged)' . \PHP_EOL; +echo $demo->styleTrueColor('foreground'); +echo \PHP_EOL . 'True Color Background (Abridged)' . \PHP_EOL; +echo $demo->styleTrueColor('background'); +echo \PHP_EOL . 'True Color Underline (Abridged)' . \PHP_EOL; +echo $demo->styleTrueColor('underline'); diff --git a/src/Style.php b/src/Style.php index da4a3c2..5ab2f9f 100644 --- a/src/Style.php +++ b/src/Style.php @@ -20,7 +20,39 @@ final class Style implements ApplierInterface */ public function __construct($resource = \STDOUT) { - $this->checkSupport($resource); + if (!\in_array(\PHP_SAPI, ['cli', 'cli-server', 'phpdbg'], true) + || getenv('NO_COLOR') !== false + ) { + $this->supportsStyles = false; + $this->supports256Colors = false; + $this->supportsRGBColors = false; + + return; + } + + if ((PHP_OS_FAMILY === 'Windows' && sapi_windows_vt100_support($resource)) + || stream_isatty($resource) || getenv('FORCE_COLOR') !== false + ) { + $this->supportsStyles = true; + + if ((PHP_OS_FAMILY === 'Windows' + && (getenv('ANSICON') !== false || getenv('ConEmuANSI') === 'ON')) + || getenv('COLORTERM') === 'truecolor' + ) { + $this->supports256Colors = true; + $this->supportsRGBColors = true; + } elseif (str_contains((string) getenv('TERM'), '256color')) { + $this->supports256Colors = true; + $this->supportsRGBColors = false; + } else { + $this->supports256Colors = false; + $this->supportsRGBColors = false; + } + } else { + $this->supportsStyles = false; + $this->supports256Colors = false; + $this->supportsRGBColors = false; + } } public function autoTerminate(bool $autoTerminate = true): void @@ -99,48 +131,6 @@ public function willAutoTerminate(): bool return $this->autoTerminate; } - /** - * Does the default output (STDOUT) support styling? - * - * @param resource $resource - */ - private function checkSupport($resource): void - { - if (!\in_array(\PHP_SAPI, ['cli', 'cli-server', 'phpdbg'], true) - || getenv('NO_COLOR') !== false - ) { - $this->supportsStyles = false; - $this->supports256Colors = false; - $this->supportsRGBColors = false; - - return; - } - - if ((PHP_OS_FAMILY === 'Windows' && sapi_windows_vt100_support($resource)) - || stream_isatty($resource) || getenv('FORCE_COLOR') !== false - ) { - $this->supportsStyles = true; - - if ((PHP_OS_FAMILY === 'Windows' - && (getenv('ANSICON') !== false || getenv('ConEmuANSI') === 'ON')) - || getenv('COLORTERM') === 'truecolor' - ) { - $this->supports256Colors = true; - $this->supportsRGBColors = true; - } elseif (str_contains((string) getenv('TERM'), '256color')) { - $this->supports256Colors = true; - $this->supportsRGBColors = false; - } else { - $this->supports256Colors = false; - $this->supportsRGBColors = false; - } - } else { - $this->supportsStyles = false; - $this->supports256Colors = false; - $this->supportsRGBColors = false; - } - } - /** * Get style terminator sequence. */