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