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