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