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 32*e43cd7e1SAndreas 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 /** 88*e43cd7e1SAndreas Gohr * Tries to figure out the width of the terminal 89*e43cd7e1SAndreas Gohr * 90*e43cd7e1SAndreas Gohr * @return int terminal width, 0 if unknown 91*e43cd7e1SAndreas Gohr */ 92*e43cd7e1SAndreas Gohr protected function getTerminalWidth() 93*e43cd7e1SAndreas Gohr { 94*e43cd7e1SAndreas Gohr // from environment 95*e43cd7e1SAndreas Gohr if (isset($_SERVER['COLUMNS'])) return (int)$_SERVER['COLUMNS']; 96*e43cd7e1SAndreas Gohr 97*e43cd7e1SAndreas Gohr // via tput 98*e43cd7e1SAndreas Gohr $process = proc_open('tput cols', array( 99*e43cd7e1SAndreas Gohr 1 => array('pipe', 'w'), 100*e43cd7e1SAndreas Gohr 2 => array('pipe', 'w'), 101*e43cd7e1SAndreas Gohr ), $pipes); 102*e43cd7e1SAndreas Gohr $width = (int)stream_get_contents($pipes[1]); 103*e43cd7e1SAndreas Gohr proc_close($process); 104*e43cd7e1SAndreas Gohr 105*e43cd7e1SAndreas Gohr return $width; 106*e43cd7e1SAndreas Gohr } 107*e43cd7e1SAndreas Gohr 108*e43cd7e1SAndreas 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 { 276cbeaa4a0SAndreas Gohr return substr($string, $start, $length); 277cbeaa4a0SAndreas Gohr } 278cbeaa4a0SAndreas Gohr } 279cbeaa4a0SAndreas Gohr 280cbeaa4a0SAndreas Gohr /** 281cbeaa4a0SAndreas Gohr * @param string $str 282cbeaa4a0SAndreas Gohr * @param int $width 283cbeaa4a0SAndreas Gohr * @param string $break 284cbeaa4a0SAndreas Gohr * @param bool $cut 285cbeaa4a0SAndreas Gohr * @return string 286cbeaa4a0SAndreas Gohr * @link http://stackoverflow.com/a/4988494 287cbeaa4a0SAndreas Gohr */ 288cbeaa4a0SAndreas Gohr protected function wordwrap($str, $width = 75, $break = "\n", $cut = false) 289cbeaa4a0SAndreas Gohr { 290cbeaa4a0SAndreas Gohr $lines = explode($break, $str); 291cbeaa4a0SAndreas Gohr foreach ($lines as &$line) { 292cbeaa4a0SAndreas Gohr $line = rtrim($line); 293cbeaa4a0SAndreas Gohr if ($this->strlen($line) <= $width) { 294cbeaa4a0SAndreas Gohr continue; 295cbeaa4a0SAndreas Gohr } 296cbeaa4a0SAndreas Gohr $words = explode(' ', $line); 297cbeaa4a0SAndreas Gohr $line = ''; 298cbeaa4a0SAndreas Gohr $actual = ''; 299cbeaa4a0SAndreas Gohr foreach ($words as $word) { 300cbeaa4a0SAndreas Gohr if ($this->strlen($actual . $word) <= $width) { 301cbeaa4a0SAndreas Gohr $actual .= $word . ' '; 302cbeaa4a0SAndreas Gohr } else { 303cbeaa4a0SAndreas Gohr if ($actual != '') { 304cbeaa4a0SAndreas Gohr $line .= rtrim($actual) . $break; 305cbeaa4a0SAndreas Gohr } 306cbeaa4a0SAndreas Gohr $actual = $word; 307cbeaa4a0SAndreas Gohr if ($cut) { 308cbeaa4a0SAndreas Gohr while ($this->strlen($actual) > $width) { 309cbeaa4a0SAndreas Gohr $line .= $this->substr($actual, 0, $width) . $break; 310cbeaa4a0SAndreas Gohr $actual = $this->substr($actual, $width); 311cbeaa4a0SAndreas Gohr } 312cbeaa4a0SAndreas Gohr } 313cbeaa4a0SAndreas Gohr $actual .= ' '; 314cbeaa4a0SAndreas Gohr } 315cbeaa4a0SAndreas Gohr } 316cbeaa4a0SAndreas Gohr $line .= trim($actual); 317cbeaa4a0SAndreas Gohr } 318cbeaa4a0SAndreas Gohr return implode($break, $lines); 319cbeaa4a0SAndreas Gohr } 320cbeaa4a0SAndreas Gohr}