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