Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
92.00% covered (success)
92.00%
23 / 25
66.67% covered (warning)
66.67%
4 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
ColorPreservingProcessEnvironmentConfigurator
92.00% covered (success)
92.00%
23 / 25
66.67% covered (warning)
66.67%
4 / 6
18.17
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 configure
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
7.04
 shouldForceColor
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 isTruthyEnvironmentFlag
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
3
 hasNoColorOptOut
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 setDefault
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
1<?php
2
3declare(strict_types=1);
4
5/**
6 * Fast Forward Development Tools for PHP projects.
7 *
8 * This file is part of fast-forward/dev-tools project.
9 *
10 * @author   Felipe SayĆ£o Lobato Abreu <github@mentordosnerds.com>
11 * @license  https://opensource.org/licenses/MIT MIT License
12 *
13 * @see      https://github.com/php-fast-forward/
14 * @see      https://github.com/php-fast-forward/dev-tools
15 * @see      https://github.com/php-fast-forward/dev-tools/issues
16 * @see      https://php-fast-forward.github.io/dev-tools/
17 * @see      https://datatracker.ietf.org/doc/html/rfc2119
18 */
19
20namespace FastForward\DevTools\Process;
21
22use FastForward\DevTools\Console\Output\OutputCapabilityDetectorInterface;
23use FastForward\DevTools\Environment\EnvironmentInterface;
24use Stringable;
25use Symfony\Component\Console\Output\OutputInterface;
26use Symfony\Component\Process\Process;
27
28/**
29 * Keeps nested process output color-friendly without requiring PTY support.
30 */
31  class ColorPreservingProcessEnvironmentConfigurator implements ProcessEnvironmentConfiguratorInterface
32{
33    /**
34     * @param EnvironmentInterface $environment reads parent process environment variables
35     * @param OutputCapabilityDetectorInterface $outputCapabilityDetector detects TTY/decorated output capabilities
36     */
37    public function __construct(
38        private EnvironmentInterface $environment,
39        private OutputCapabilityDetectorInterface $outputCapabilityDetector,
40    ) {}
41
42    /**
43     * Configures color-related environment variables for nested commands.
44     *
45     * @param Process $process the queued process that will be started
46     * @param OutputInterface $output the parent output used to infer console capabilities
47     */
48    public function configure(Process $process, OutputInterface $output): void
49    {
50        if (! $this->shouldForceColor($output)) {
51            return;
52        }
53
54        $env = $process->getEnv();
55
56        if ($this->hasNoColorOptOut($env)) {
57            return;
58        }
59
60        $changed = $this->setDefault($env, 'FORCE_COLOR', '1');
61        $changed = $this->setDefault($env, 'CLICOLOR_FORCE', '1') || $changed;
62
63        if (null !== ($term = $this->environment->get('TERM'))) {
64            $changed = $this->setDefault($env, 'TERM', $term) || $changed;
65        }
66
67        if ($changed) {
68            $process->setEnv($env);
69        }
70    }
71
72    /**
73     * Determines whether child processes should be nudged toward ANSI output.
74     *
75     * @param OutputInterface $output the parent process output
76     *
77     * @return bool true when color should be forced for child processes
78     */
79    private function shouldForceColor(OutputInterface $output): bool
80    {
81        if ($this->outputCapabilityDetector->supportsAnsi($output)) {
82            return true;
83        }
84
85        if ($this->isTruthyEnvironmentFlag('FORCE_COLOR')) {
86            return true;
87        }
88
89        return $this->isTruthyEnvironmentFlag('CLICOLOR_FORCE');
90    }
91
92    /**
93     * Determines whether an environment flag is set to a truthy value.
94     *
95     * @param string $name the environment variable name
96     *
97     * @return bool true when the environment variable is truthy
98     */
99    private function isTruthyEnvironmentFlag(string $name): bool
100    {
101        $value = $this->environment->get($name, '');
102
103        return null !== $value && '' !== $value && '0' !== $value;
104    }
105
106    /**
107     * Determines whether the process or parent environment opted out of color.
108     *
109     * @param array<string|Stringable> $env the process-specific environment variables
110     *
111     * @return bool true when NO_COLOR is present
112     */
113    private function hasNoColorOptOut(array $env): bool
114    {
115        return \array_key_exists('NO_COLOR', $env)
116            || null !== $this->environment->get('NO_COLOR');
117    }
118
119    /**
120     * Sets an environment default while preserving caller-provided values.
121     *
122     * @param array<string|Stringable> $env the environment map to update
123     * @param string $name the environment variable name
124     * @param string $value the default value
125     *
126     * @return bool true when the environment map changed
127     */
128    private function setDefault(array &$env, string $name, string $value): bool
129    {
130        if (\array_key_exists($name, $env)) {
131            return false;
132        }
133
134        $env[$name] = $value;
135
136        return true;
137    }
138}