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    /** @var array known color names */
35    protected $colors = array(
36        self::C_RESET => "\33[0m",
37        self::C_BLACK => "\33[0;30m",
38        self::C_DARKGRAY => "\33[1;30m",
39        self::C_BLUE => "\33[0;34m",
40        self::C_LIGHTBLUE => "\33[1;34m",
41        self::C_GREEN => "\33[0;32m",
42        self::C_LIGHTGREEN => "\33[1;32m",
43        self::C_CYAN => "\33[0;36m",
44        self::C_LIGHTCYAN => "\33[1;36m",
45        self::C_RED => "\33[0;31m",
46        self::C_LIGHTRED => "\33[1;31m",
47        self::C_PURPLE => "\33[0;35m",
48        self::C_LIGHTPURPLE => "\33[1;35m",
49        self::C_BROWN => "\33[0;33m",
50        self::C_YELLOW => "\33[1;33m",
51        self::C_LIGHTGRAY => "\33[0;37m",
52        self::C_WHITE => "\33[1;37m",
53    );
54
55    /** @var bool should colors be used? */
56    protected $enabled = true;
57
58    /**
59     * Constructor
60     *
61     * Tries to disable colors for non-terminals
62     */
63    public function __construct()
64    {
65        if (function_exists('posix_isatty') && !posix_isatty(STDOUT)) {
66            $this->enabled = false;
67            return;
68        }
69        if (!getenv('TERM')) {
70            $this->enabled = false;
71            return;
72        }
73        if (getenv('NO_COLOR')) { // https://no-color.org/
74            $this->enabled = false;
75            return;
76        }
77    }
78
79    /**
80     * enable color output
81     */
82    public function enable()
83    {
84        $this->enabled = true;
85    }
86
87    /**
88     * disable color output
89     */
90    public function disable()
91    {
92        $this->enabled = false;
93    }
94
95    /**
96     * @return bool is color support enabled?
97     */
98    public function isEnabled()
99    {
100        return $this->enabled;
101    }
102
103    /**
104     * Convenience function to print a line in a given color
105     *
106     * @param string   $line    the line to print, a new line is added automatically
107     * @param string   $color   one of the available color names
108     * @param resource $channel file descriptor to write to
109     *
110     * @throws Exception
111     */
112    public function ptln($line, $color, $channel = STDOUT)
113    {
114        $this->set($color, $channel);
115        fwrite($channel, rtrim($line) . "\n");
116        $this->reset($channel);
117    }
118
119    /**
120     * Returns the given text wrapped in the appropriate color and reset code
121     *
122     * @param string $text string to wrap
123     * @param string $color one of the available color names
124     * @return string the wrapped string
125     * @throws Exception
126     */
127    public function wrap($text, $color)
128    {
129        return $this->getColorCode($color) . $text . $this->getColorCode('reset');
130    }
131
132    /**
133     * Gets the appropriate terminal code for the given color
134     *
135     * @param string $color one of the available color names
136     * @return string color code
137     * @throws Exception
138     */
139    public function getColorCode($color)
140    {
141        if (!$this->enabled) {
142            return '';
143        }
144        if (!isset($this->colors[$color])) {
145            throw new Exception("No such color $color");
146        }
147
148        return $this->colors[$color];
149    }
150
151    /**
152     * Set the given color for consecutive output
153     *
154     * @param string $color one of the supported color names
155     * @param resource $channel file descriptor to write to
156     * @throws Exception
157     */
158    public function set($color, $channel = STDOUT)
159    {
160        fwrite($channel, $this->getColorCode($color));
161    }
162
163    /**
164     * reset the terminal color
165     *
166     * @param resource $channel file descriptor to write to
167     *
168     * @throws Exception
169     */
170    public function reset($channel = STDOUT)
171    {
172        $this->set('reset', $channel);
173    }
174}
175