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