1<?php
2
3namespace splitbrain\phpcli;
4
5/**
6 * Class Colors
7 *
8 * Handles color output on (Linux) terminals
9 *
10 * @author Andreas Gohr <andi@splitbrain.org>
11 * @license MIT
12 */
13class Colors
14{
15    // these constants make IDE autocompletion easier, but color names can also be passed as strings
16    const C_RESET = 'reset';
17    const C_BLACK = 'black';
18    const C_DARKGRAY = 'darkgray';
19    const C_BLUE = 'blue';
20    const C_LIGHTBLUE = 'lightblue';
21    const C_GREEN = 'green';
22    const C_LIGHTGREEN = 'lightgreen';
23    const C_CYAN = 'cyan';
24    const C_LIGHTCYAN = 'lightcyan';
25    const C_RED = 'red';
26    const C_LIGHTRED = 'lightred';
27    const C_PURPLE = 'purple';
28    const C_LIGHTPURPLE = 'lightpurple';
29    const C_BROWN = 'brown';
30    const C_YELLOW = 'yellow';
31    const C_LIGHTGRAY = 'lightgray';
32    const C_WHITE = 'white';
33
34    // Regex pattern to match color codes
35    const C_CODE_REGEX = "/(\33\[[0-9;]+m)/";
36
37    /** @var array known color names */
38    protected $colors = array(
39        self::C_RESET => "\33[0m",
40        self::C_BLACK => "\33[0;30m",
41        self::C_DARKGRAY => "\33[1;30m",
42        self::C_BLUE => "\33[0;34m",
43        self::C_LIGHTBLUE => "\33[1;34m",
44        self::C_GREEN => "\33[0;32m",
45        self::C_LIGHTGREEN => "\33[1;32m",
46        self::C_CYAN => "\33[0;36m",
47        self::C_LIGHTCYAN => "\33[1;36m",
48        self::C_RED => "\33[0;31m",
49        self::C_LIGHTRED => "\33[1;31m",
50        self::C_PURPLE => "\33[0;35m",
51        self::C_LIGHTPURPLE => "\33[1;35m",
52        self::C_BROWN => "\33[0;33m",
53        self::C_YELLOW => "\33[1;33m",
54        self::C_LIGHTGRAY => "\33[0;37m",
55        self::C_WHITE => "\33[1;37m",
56    );
57
58    /** @var bool should colors be used? */
59    protected $enabled = true;
60
61    /**
62     * Constructor
63     *
64     * Tries to disable colors for non-terminals
65     */
66    public function __construct()
67    {
68        if (function_exists('posix_isatty') && !posix_isatty(STDOUT)) {
69            $this->enabled = false;
70            return;
71        }
72        if (!getenv('TERM')) {
73            $this->enabled = false;
74            return;
75        }
76        if (getenv('NO_COLOR')) { // https://no-color.org/
77            $this->enabled = false;
78            return;
79        }
80    }
81
82    /**
83     * enable color output
84     */
85    public function enable()
86    {
87        $this->enabled = true;
88    }
89
90    /**
91     * disable color output
92     */
93    public function disable()
94    {
95        $this->enabled = false;
96    }
97
98    /**
99     * @return bool is color support enabled?
100     */
101    public function isEnabled()
102    {
103        return $this->enabled;
104    }
105
106    /**
107     * Convenience function to print a line in a given color
108     *
109     * @param string   $line    the line to print, a new line is added automatically
110     * @param string   $color   one of the available color names
111     * @param resource $channel file descriptor to write to
112     *
113     * @throws Exception
114     */
115    public function ptln($line, $color, $channel = STDOUT)
116    {
117        $this->set($color, $channel);
118        fwrite($channel, rtrim($line) . "\n");
119        $this->reset($channel);
120    }
121
122    /**
123     * Returns the given text wrapped in the appropriate color and reset code
124     *
125     * @param string $text string to wrap
126     * @param string $color one of the available color names
127     * @return string the wrapped string
128     * @throws Exception
129     */
130    public function wrap($text, $color)
131    {
132        return $this->getColorCode($color) . $text . $this->getColorCode('reset');
133    }
134
135    /**
136     * Gets the appropriate terminal code for the given color
137     *
138     * @param string $color one of the available color names
139     * @return string color code
140     * @throws Exception
141     */
142    public function getColorCode($color)
143    {
144        if (!$this->enabled) {
145            return '';
146        }
147        if (!isset($this->colors[$color])) {
148            throw new Exception("No such color $color");
149        }
150
151        return $this->colors[$color];
152    }
153
154    /**
155     * Set the given color for consecutive output
156     *
157     * @param string $color one of the supported color names
158     * @param resource $channel file descriptor to write to
159     * @throws Exception
160     */
161    public function set($color, $channel = STDOUT)
162    {
163        fwrite($channel, $this->getColorCode($color));
164    }
165
166    /**
167     * reset the terminal color
168     *
169     * @param resource $channel file descriptor to write to
170     *
171     * @throws Exception
172     */
173    public function reset($channel = STDOUT)
174    {
175        $this->set('reset', $channel);
176    }
177}
178