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