xref: /plugin/upgrade/myvendor/splitbrain/php-cli/src/TableFormatter.php (revision ab8e5256dbaece2751c4414d6cf7b761cb71998a)
1*ab8e5256SAndreas Gohr<?php
2*ab8e5256SAndreas Gohr
3*ab8e5256SAndreas Gohrnamespace splitbrain\phpcli;
4*ab8e5256SAndreas Gohr
5*ab8e5256SAndreas Gohr/**
6*ab8e5256SAndreas Gohr * Class TableFormatter
7*ab8e5256SAndreas Gohr *
8*ab8e5256SAndreas Gohr * Output text in multiple columns
9*ab8e5256SAndreas Gohr *
10*ab8e5256SAndreas Gohr * @author Andreas Gohr <andi@splitbrain.org>
11*ab8e5256SAndreas Gohr * @license MIT
12*ab8e5256SAndreas Gohr */
13*ab8e5256SAndreas Gohrclass TableFormatter
14*ab8e5256SAndreas Gohr{
15*ab8e5256SAndreas Gohr    /** @var string border between columns */
16*ab8e5256SAndreas Gohr    protected $border = ' ';
17*ab8e5256SAndreas Gohr
18*ab8e5256SAndreas Gohr    /** @var int the terminal width */
19*ab8e5256SAndreas Gohr    protected $max = 74;
20*ab8e5256SAndreas Gohr
21*ab8e5256SAndreas Gohr    /** @var Colors for coloring output */
22*ab8e5256SAndreas Gohr    protected $colors;
23*ab8e5256SAndreas Gohr
24*ab8e5256SAndreas Gohr    /**
25*ab8e5256SAndreas Gohr     * TableFormatter constructor.
26*ab8e5256SAndreas Gohr     *
27*ab8e5256SAndreas Gohr     * @param Colors|null $colors
28*ab8e5256SAndreas Gohr     */
29*ab8e5256SAndreas Gohr    public function __construct(Colors $colors = null)
30*ab8e5256SAndreas Gohr    {
31*ab8e5256SAndreas Gohr        // try to get terminal width
32*ab8e5256SAndreas Gohr        $width = $this->getTerminalWidth();
33*ab8e5256SAndreas Gohr        if ($width) {
34*ab8e5256SAndreas Gohr            $this->max = $width - 1;
35*ab8e5256SAndreas Gohr        }
36*ab8e5256SAndreas Gohr
37*ab8e5256SAndreas Gohr        if ($colors) {
38*ab8e5256SAndreas Gohr            $this->colors = $colors;
39*ab8e5256SAndreas Gohr        } else {
40*ab8e5256SAndreas Gohr            $this->colors = new Colors();
41*ab8e5256SAndreas Gohr        }
42*ab8e5256SAndreas Gohr    }
43*ab8e5256SAndreas Gohr
44*ab8e5256SAndreas Gohr    /**
45*ab8e5256SAndreas Gohr     * The currently set border (defaults to ' ')
46*ab8e5256SAndreas Gohr     *
47*ab8e5256SAndreas Gohr     * @return string
48*ab8e5256SAndreas Gohr     */
49*ab8e5256SAndreas Gohr    public function getBorder()
50*ab8e5256SAndreas Gohr    {
51*ab8e5256SAndreas Gohr        return $this->border;
52*ab8e5256SAndreas Gohr    }
53*ab8e5256SAndreas Gohr
54*ab8e5256SAndreas Gohr    /**
55*ab8e5256SAndreas Gohr     * Set the border. The border is set between each column. Its width is
56*ab8e5256SAndreas Gohr     * added to the column widths.
57*ab8e5256SAndreas Gohr     *
58*ab8e5256SAndreas Gohr     * @param string $border
59*ab8e5256SAndreas Gohr     */
60*ab8e5256SAndreas Gohr    public function setBorder($border)
61*ab8e5256SAndreas Gohr    {
62*ab8e5256SAndreas Gohr        $this->border = $border;
63*ab8e5256SAndreas Gohr    }
64*ab8e5256SAndreas Gohr
65*ab8e5256SAndreas Gohr    /**
66*ab8e5256SAndreas Gohr     * Width of the terminal in characters
67*ab8e5256SAndreas Gohr     *
68*ab8e5256SAndreas Gohr     * initially autodetected
69*ab8e5256SAndreas Gohr     *
70*ab8e5256SAndreas Gohr     * @return int
71*ab8e5256SAndreas Gohr     */
72*ab8e5256SAndreas Gohr    public function getMaxWidth()
73*ab8e5256SAndreas Gohr    {
74*ab8e5256SAndreas Gohr        return $this->max;
75*ab8e5256SAndreas Gohr    }
76*ab8e5256SAndreas Gohr
77*ab8e5256SAndreas Gohr    /**
78*ab8e5256SAndreas Gohr     * Set the width of the terminal to assume (in characters)
79*ab8e5256SAndreas Gohr     *
80*ab8e5256SAndreas Gohr     * @param int $max
81*ab8e5256SAndreas Gohr     */
82*ab8e5256SAndreas Gohr    public function setMaxWidth($max)
83*ab8e5256SAndreas Gohr    {
84*ab8e5256SAndreas Gohr        $this->max = $max;
85*ab8e5256SAndreas Gohr    }
86*ab8e5256SAndreas Gohr
87*ab8e5256SAndreas Gohr    /**
88*ab8e5256SAndreas Gohr     * Tries to figure out the width of the terminal
89*ab8e5256SAndreas Gohr     *
90*ab8e5256SAndreas Gohr     * @return int terminal width, 0 if unknown
91*ab8e5256SAndreas Gohr     */
92*ab8e5256SAndreas Gohr    protected function getTerminalWidth()
93*ab8e5256SAndreas Gohr    {
94*ab8e5256SAndreas Gohr        // from environment
95*ab8e5256SAndreas Gohr        if (isset($_SERVER['COLUMNS'])) return (int)$_SERVER['COLUMNS'];
96*ab8e5256SAndreas Gohr
97*ab8e5256SAndreas Gohr        // via tput
98*ab8e5256SAndreas Gohr        $process = proc_open('tput cols', array(
99*ab8e5256SAndreas Gohr            1 => array('pipe', 'w'),
100*ab8e5256SAndreas Gohr            2 => array('pipe', 'w'),
101*ab8e5256SAndreas Gohr        ), $pipes);
102*ab8e5256SAndreas Gohr        $width = (int)stream_get_contents($pipes[1]);
103*ab8e5256SAndreas Gohr        proc_close($process);
104*ab8e5256SAndreas Gohr
105*ab8e5256SAndreas Gohr        return $width;
106*ab8e5256SAndreas Gohr    }
107*ab8e5256SAndreas Gohr
108*ab8e5256SAndreas Gohr    /**
109*ab8e5256SAndreas Gohr     * Takes an array with dynamic column width and calculates the correct width
110*ab8e5256SAndreas Gohr     *
111*ab8e5256SAndreas Gohr     * Column width can be given as fixed char widths, percentages and a single * width can be given
112*ab8e5256SAndreas Gohr     * for taking the remaining available space. When mixing percentages and fixed widths, percentages
113*ab8e5256SAndreas Gohr     * refer to the remaining space after allocating the fixed width
114*ab8e5256SAndreas Gohr     *
115*ab8e5256SAndreas Gohr     * @param array $columns
116*ab8e5256SAndreas Gohr     * @return int[]
117*ab8e5256SAndreas Gohr     * @throws Exception
118*ab8e5256SAndreas Gohr     */
119*ab8e5256SAndreas Gohr    protected function calculateColLengths($columns)
120*ab8e5256SAndreas Gohr    {
121*ab8e5256SAndreas Gohr        $idx = 0;
122*ab8e5256SAndreas Gohr        $border = $this->strlen($this->border);
123*ab8e5256SAndreas Gohr        $fixed = (count($columns) - 1) * $border; // borders are used already
124*ab8e5256SAndreas Gohr        $fluid = -1;
125*ab8e5256SAndreas Gohr
126*ab8e5256SAndreas Gohr        // first pass for format check and fixed columns
127*ab8e5256SAndreas Gohr        foreach ($columns as $idx => $col) {
128*ab8e5256SAndreas Gohr            // handle fixed columns
129*ab8e5256SAndreas Gohr            if ((string)intval($col) === (string)$col) {
130*ab8e5256SAndreas Gohr                $fixed += $col;
131*ab8e5256SAndreas Gohr                continue;
132*ab8e5256SAndreas Gohr            }
133*ab8e5256SAndreas Gohr            // check if other colums are using proper units
134*ab8e5256SAndreas Gohr            if (substr($col, -1) == '%') {
135*ab8e5256SAndreas Gohr                continue;
136*ab8e5256SAndreas Gohr            }
137*ab8e5256SAndreas Gohr            if ($col == '*') {
138*ab8e5256SAndreas Gohr                // only one fluid
139*ab8e5256SAndreas Gohr                if ($fluid < 0) {
140*ab8e5256SAndreas Gohr                    $fluid = $idx;
141*ab8e5256SAndreas Gohr                    continue;
142*ab8e5256SAndreas Gohr                } else {
143*ab8e5256SAndreas Gohr                    throw new Exception('Only one fluid column allowed!');
144*ab8e5256SAndreas Gohr                }
145*ab8e5256SAndreas Gohr            }
146*ab8e5256SAndreas Gohr            throw new Exception("unknown column format $col");
147*ab8e5256SAndreas Gohr        }
148*ab8e5256SAndreas Gohr
149*ab8e5256SAndreas Gohr        $alloc = $fixed;
150*ab8e5256SAndreas Gohr        $remain = $this->max - $alloc;
151*ab8e5256SAndreas Gohr
152*ab8e5256SAndreas Gohr        // second pass to handle percentages
153*ab8e5256SAndreas Gohr        foreach ($columns as $idx => $col) {
154*ab8e5256SAndreas Gohr            if (substr($col, -1) != '%') {
155*ab8e5256SAndreas Gohr                continue;
156*ab8e5256SAndreas Gohr            }
157*ab8e5256SAndreas Gohr            $perc = floatval($col);
158*ab8e5256SAndreas Gohr
159*ab8e5256SAndreas Gohr            $real = (int)floor(($perc * $remain) / 100);
160*ab8e5256SAndreas Gohr
161*ab8e5256SAndreas Gohr            $columns[$idx] = $real;
162*ab8e5256SAndreas Gohr            $alloc += $real;
163*ab8e5256SAndreas Gohr        }
164*ab8e5256SAndreas Gohr
165*ab8e5256SAndreas Gohr        $remain = $this->max - $alloc;
166*ab8e5256SAndreas Gohr        if ($remain < 0) {
167*ab8e5256SAndreas Gohr            throw new Exception("Wanted column widths exceed available space");
168*ab8e5256SAndreas Gohr        }
169*ab8e5256SAndreas Gohr
170*ab8e5256SAndreas Gohr        // assign remaining space
171*ab8e5256SAndreas Gohr        if ($fluid < 0) {
172*ab8e5256SAndreas Gohr            $columns[$idx] += ($remain); // add to last column
173*ab8e5256SAndreas Gohr        } else {
174*ab8e5256SAndreas Gohr            $columns[$fluid] = $remain;
175*ab8e5256SAndreas Gohr        }
176*ab8e5256SAndreas Gohr
177*ab8e5256SAndreas Gohr        return $columns;
178*ab8e5256SAndreas Gohr    }
179*ab8e5256SAndreas Gohr
180*ab8e5256SAndreas Gohr    /**
181*ab8e5256SAndreas Gohr     * Displays text in multiple word wrapped columns
182*ab8e5256SAndreas Gohr     *
183*ab8e5256SAndreas Gohr     * @param int[] $columns list of column widths (in characters, percent or '*')
184*ab8e5256SAndreas Gohr     * @param string[] $texts list of texts for each column
185*ab8e5256SAndreas Gohr     * @param array $colors A list of color names to use for each column. use empty string for default
186*ab8e5256SAndreas Gohr     * @return string
187*ab8e5256SAndreas Gohr     * @throws Exception
188*ab8e5256SAndreas Gohr     */
189*ab8e5256SAndreas Gohr    public function format($columns, $texts, $colors = array())
190*ab8e5256SAndreas Gohr    {
191*ab8e5256SAndreas Gohr        $columns = $this->calculateColLengths($columns);
192*ab8e5256SAndreas Gohr
193*ab8e5256SAndreas Gohr        $wrapped = array();
194*ab8e5256SAndreas Gohr        $maxlen = 0;
195*ab8e5256SAndreas Gohr
196*ab8e5256SAndreas Gohr        foreach ($columns as $col => $width) {
197*ab8e5256SAndreas Gohr            $wrapped[$col] = explode("\n", $this->wordwrap($texts[$col], $width, "\n", true));
198*ab8e5256SAndreas Gohr            $len = count($wrapped[$col]);
199*ab8e5256SAndreas Gohr            if ($len > $maxlen) {
200*ab8e5256SAndreas Gohr                $maxlen = $len;
201*ab8e5256SAndreas Gohr            }
202*ab8e5256SAndreas Gohr
203*ab8e5256SAndreas Gohr        }
204*ab8e5256SAndreas Gohr
205*ab8e5256SAndreas Gohr        $last = count($columns) - 1;
206*ab8e5256SAndreas Gohr        $out = '';
207*ab8e5256SAndreas Gohr        for ($i = 0; $i < $maxlen; $i++) {
208*ab8e5256SAndreas Gohr            foreach ($columns as $col => $width) {
209*ab8e5256SAndreas Gohr                if (isset($wrapped[$col][$i])) {
210*ab8e5256SAndreas Gohr                    $val = $wrapped[$col][$i];
211*ab8e5256SAndreas Gohr                } else {
212*ab8e5256SAndreas Gohr                    $val = '';
213*ab8e5256SAndreas Gohr                }
214*ab8e5256SAndreas Gohr                $chunk = $this->pad($val, $width);
215*ab8e5256SAndreas Gohr                if (isset($colors[$col]) && $colors[$col]) {
216*ab8e5256SAndreas Gohr                    $chunk = $this->colors->wrap($chunk, $colors[$col]);
217*ab8e5256SAndreas Gohr                }
218*ab8e5256SAndreas Gohr                $out .= $chunk;
219*ab8e5256SAndreas Gohr
220*ab8e5256SAndreas Gohr                // border
221*ab8e5256SAndreas Gohr                if ($col != $last) {
222*ab8e5256SAndreas Gohr                    $out .= $this->border;
223*ab8e5256SAndreas Gohr                }
224*ab8e5256SAndreas Gohr            }
225*ab8e5256SAndreas Gohr            $out .= "\n";
226*ab8e5256SAndreas Gohr        }
227*ab8e5256SAndreas Gohr        return $out;
228*ab8e5256SAndreas Gohr
229*ab8e5256SAndreas Gohr    }
230*ab8e5256SAndreas Gohr
231*ab8e5256SAndreas Gohr    /**
232*ab8e5256SAndreas Gohr     * Pad the given string to the correct length
233*ab8e5256SAndreas Gohr     *
234*ab8e5256SAndreas Gohr     * @param string $string
235*ab8e5256SAndreas Gohr     * @param int $len
236*ab8e5256SAndreas Gohr     * @return string
237*ab8e5256SAndreas Gohr     */
238*ab8e5256SAndreas Gohr    protected function pad($string, $len)
239*ab8e5256SAndreas Gohr    {
240*ab8e5256SAndreas Gohr        $strlen = $this->strlen($string);
241*ab8e5256SAndreas Gohr        if ($strlen > $len) return $string;
242*ab8e5256SAndreas Gohr
243*ab8e5256SAndreas Gohr        $pad = $len - $strlen;
244*ab8e5256SAndreas Gohr        return $string . str_pad('', $pad, ' ');
245*ab8e5256SAndreas Gohr    }
246*ab8e5256SAndreas Gohr
247*ab8e5256SAndreas Gohr    /**
248*ab8e5256SAndreas Gohr     * Measures char length in UTF-8 when possible
249*ab8e5256SAndreas Gohr     *
250*ab8e5256SAndreas Gohr     * @param $string
251*ab8e5256SAndreas Gohr     * @return int
252*ab8e5256SAndreas Gohr     */
253*ab8e5256SAndreas Gohr    protected function strlen($string)
254*ab8e5256SAndreas Gohr    {
255*ab8e5256SAndreas Gohr        // don't count color codes
256*ab8e5256SAndreas Gohr        $string = preg_replace("/\33\\[\\d+(;\\d+)?m/", '', $string);
257*ab8e5256SAndreas Gohr
258*ab8e5256SAndreas Gohr        if (function_exists('mb_strlen')) {
259*ab8e5256SAndreas Gohr            return mb_strlen($string, 'utf-8');
260*ab8e5256SAndreas Gohr        }
261*ab8e5256SAndreas Gohr
262*ab8e5256SAndreas Gohr        return strlen($string);
263*ab8e5256SAndreas Gohr    }
264*ab8e5256SAndreas Gohr
265*ab8e5256SAndreas Gohr    /**
266*ab8e5256SAndreas Gohr     * @param string $string
267*ab8e5256SAndreas Gohr     * @param int $start
268*ab8e5256SAndreas Gohr     * @param int|null $length
269*ab8e5256SAndreas Gohr     * @return string
270*ab8e5256SAndreas Gohr     */
271*ab8e5256SAndreas Gohr    protected function substr($string, $start = 0, $length = null)
272*ab8e5256SAndreas Gohr    {
273*ab8e5256SAndreas Gohr        if (function_exists('mb_substr')) {
274*ab8e5256SAndreas Gohr            return mb_substr($string, $start, $length);
275*ab8e5256SAndreas Gohr        } else {
276*ab8e5256SAndreas Gohr            // mb_substr() treats $length differently than substr()
277*ab8e5256SAndreas Gohr            if ($length) {
278*ab8e5256SAndreas Gohr                return substr($string, $start, $length);
279*ab8e5256SAndreas Gohr            } else {
280*ab8e5256SAndreas Gohr                return substr($string, $start);
281*ab8e5256SAndreas Gohr            }
282*ab8e5256SAndreas Gohr        }
283*ab8e5256SAndreas Gohr    }
284*ab8e5256SAndreas Gohr
285*ab8e5256SAndreas Gohr    /**
286*ab8e5256SAndreas Gohr     * @param string $str
287*ab8e5256SAndreas Gohr     * @param int $width
288*ab8e5256SAndreas Gohr     * @param string $break
289*ab8e5256SAndreas Gohr     * @param bool $cut
290*ab8e5256SAndreas Gohr     * @return string
291*ab8e5256SAndreas Gohr     * @link http://stackoverflow.com/a/4988494
292*ab8e5256SAndreas Gohr     */
293*ab8e5256SAndreas Gohr    protected function wordwrap($str, $width = 75, $break = "\n", $cut = false)
294*ab8e5256SAndreas Gohr    {
295*ab8e5256SAndreas Gohr        $lines = explode($break, $str);
296*ab8e5256SAndreas Gohr        $color_reset = $this->colors->getColorCode(Colors::C_RESET);
297*ab8e5256SAndreas Gohr        foreach ($lines as &$line) {
298*ab8e5256SAndreas Gohr            $line = rtrim($line);
299*ab8e5256SAndreas Gohr            if ($this->strlen($line) <= $width) {
300*ab8e5256SAndreas Gohr                continue;
301*ab8e5256SAndreas Gohr            }
302*ab8e5256SAndreas Gohr            $words = explode(' ', $line);
303*ab8e5256SAndreas Gohr            $line = '';
304*ab8e5256SAndreas Gohr            $actual = '';
305*ab8e5256SAndreas Gohr            $color = '';
306*ab8e5256SAndreas Gohr            foreach ($words as $word) {
307*ab8e5256SAndreas Gohr                if (preg_match_all(Colors::C_CODE_REGEX, $word, $color_codes) ) {
308*ab8e5256SAndreas Gohr                    # Word contains color codes
309*ab8e5256SAndreas Gohr                    foreach ($color_codes[0] as $code) {
310*ab8e5256SAndreas Gohr                        if ($code == $color_reset) {
311*ab8e5256SAndreas Gohr                            $color = '';
312*ab8e5256SAndreas Gohr                        } else {
313*ab8e5256SAndreas Gohr                            # Remember color so we can reapply it after a line break
314*ab8e5256SAndreas Gohr                            $color = $code;
315*ab8e5256SAndreas Gohr                        }
316*ab8e5256SAndreas Gohr                    }
317*ab8e5256SAndreas Gohr                }
318*ab8e5256SAndreas Gohr                if ($this->strlen($actual . $word) <= $width) {
319*ab8e5256SAndreas Gohr                    $actual .= $word . ' ';
320*ab8e5256SAndreas Gohr                } else {
321*ab8e5256SAndreas Gohr                    if ($actual != '') {
322*ab8e5256SAndreas Gohr                        $line .= rtrim($actual) . $break;
323*ab8e5256SAndreas Gohr                    }
324*ab8e5256SAndreas Gohr                    $actual = $color . $word;
325*ab8e5256SAndreas Gohr                    if ($cut) {
326*ab8e5256SAndreas Gohr                        while ($this->strlen($actual) > $width) {
327*ab8e5256SAndreas Gohr                            $line .= $this->substr($actual, 0, $width) . $break;
328*ab8e5256SAndreas Gohr                            $actual = $color . $this->substr($actual, $width);
329*ab8e5256SAndreas Gohr                        }
330*ab8e5256SAndreas Gohr                    }
331*ab8e5256SAndreas Gohr                    $actual .= ' ';
332*ab8e5256SAndreas Gohr                }
333*ab8e5256SAndreas Gohr            }
334*ab8e5256SAndreas Gohr            $line .= trim($actual);
335*ab8e5256SAndreas Gohr        }
336*ab8e5256SAndreas Gohr        return implode($break, $lines);
337*ab8e5256SAndreas Gohr    }
338*ab8e5256SAndreas Gohr}
339