diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -125,14 +125,18 @@ 'PhutilCodeSnippetContextFreeGrammar' => 'grammar/code/PhutilCodeSnippetContextFreeGrammar.php', 'PhutilCommandString' => 'xsprintf/PhutilCommandString.php', 'PhutilConsole' => 'console/PhutilConsole.php', + 'PhutilConsoleBlock' => 'console/view/PhutilConsoleBlock.php', + 'PhutilConsoleConcatenatedView' => 'console/view/PhutilConsoleConcatenatedView.php', 'PhutilConsoleFormatter' => 'console/PhutilConsoleFormatter.php', + 'PhutilConsoleList' => 'console/view/PhutilConsoleList.php', 'PhutilConsoleMessage' => 'console/PhutilConsoleMessage.php', 'PhutilConsoleProgressBar' => 'console/PhutilConsoleProgressBar.php', 'PhutilConsoleServer' => 'console/PhutilConsoleServer.php', 'PhutilConsoleServerChannel' => 'console/PhutilConsoleServerChannel.php', 'PhutilConsoleStdinNotInteractiveException' => 'console/PhutilConsoleStdinNotInteractiveException.php', 'PhutilConsoleSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilConsoleSyntaxHighlighter.php', - 'PhutilConsoleTable' => 'console/PhutilConsoleTable.php', + 'PhutilConsoleTable' => 'console/view/PhutilConsoleTable.php', + 'PhutilConsoleView' => 'console/view/PhutilConsoleView.php', 'PhutilConsoleWrapTestCase' => 'console/__tests__/PhutilConsoleWrapTestCase.php', 'PhutilContextFreeGrammar' => 'grammar/PhutilContextFreeGrammar.php', 'PhutilCowsay' => 'utils/PhutilCowsay.php', @@ -635,14 +639,18 @@ 'PhutilCodeSnippetContextFreeGrammar' => 'PhutilContextFreeGrammar', 'PhutilCommandString' => 'Phobject', 'PhutilConsole' => 'Phobject', + 'PhutilConsoleBlock' => 'PhutilConsoleView', + 'PhutilConsoleConcatenatedView' => 'PhutilConsoleView', 'PhutilConsoleFormatter' => 'Phobject', + 'PhutilConsoleList' => 'PhutilConsoleView', 'PhutilConsoleMessage' => 'Phobject', 'PhutilConsoleProgressBar' => 'Phobject', 'PhutilConsoleServer' => 'Phobject', 'PhutilConsoleServerChannel' => 'PhutilChannelChannel', 'PhutilConsoleStdinNotInteractiveException' => 'Exception', 'PhutilConsoleSyntaxHighlighter' => 'Phobject', - 'PhutilConsoleTable' => 'Phobject', + 'PhutilConsoleTable' => 'PhutilConsoleView', + 'PhutilConsoleView' => 'Phobject', 'PhutilConsoleWrapTestCase' => 'PhutilTestCase', 'PhutilContextFreeGrammar' => 'Phobject', 'PhutilCowsay' => 'Phobject', diff --git a/src/console/PhutilConsoleFormatter.php b/src/console/PhutilConsoleFormatter.php --- a/src/console/PhutilConsoleFormatter.php +++ b/src/console/PhutilConsoleFormatter.php @@ -42,6 +42,24 @@ } public static function formatString($format /* ... */) { + $args = func_get_args(); + $args[0] = self::interpretFormat($args[0]); + return call_user_func_array('sprintf', $args); + } + + public static function replaceColorCode($matches) { + $codes = self::$colorCodes; + $offset = 30 + $codes[$matches[2]]; + $default = 39; + if ($matches[1] == 'bg') { + $offset += 10; + $default += 10; + } + + return chr(27).'['.$offset.'m'.$matches[3].chr(27).'['.$default.'m'; + } + + public static function interpretFormat($format) { $colors = implode('|', array_keys(self::$colorCodes)); // Sequence should be preceded by start-of-string or non-backslash @@ -74,24 +92,7 @@ } // Remove backslash escaping - $format = preg_replace('/\\\\(\*\*.*\*\*|__.*__|##.*##)/sU', '\1', $format); - - $args = func_get_args(); - $args[0] = $format; - - return call_user_func_array('sprintf', $args); - } - - public static function replaceColorCode($matches) { - $codes = self::$colorCodes; - $offset = 30 + $codes[$matches[2]]; - $default = 39; - if ($matches[1] == 'bg') { - $offset += 10; - $default += 10; - } - - return chr(27).'['.$offset.'m'.$matches[3].chr(27).'['.$default.'m'; + return preg_replace('/\\\\(\*\*.*\*\*|__.*__|##.*##)/sU', '\1', $format); } } diff --git a/src/console/view/PhutilConsoleBlock.php b/src/console/view/PhutilConsoleBlock.php new file mode 100644 --- /dev/null +++ b/src/console/view/PhutilConsoleBlock.php @@ -0,0 +1,45 @@ +items[] = array( + 'type' => 'paragraph', + 'item' => $item, + ); + return $this; + } + + public function addList(PhutilConsoleList $list) { + $this->items[] = array( + 'type' => 'list', + 'item' => $list, + ); + return $this; + } + + protected function drawView() { + $output = array(); + + foreach ($this->items as $spec) { + $type = $spec['type']; + $item = $spec['item']; + + switch ($type) { + case 'paragraph': + $item = phutil_console_wrap($item)."\n"; + break; + case 'list': + $item = $item; + break; + } + + $output[] = $item; + } + + return $this->drawLines($output); + } + +} diff --git a/src/console/view/PhutilConsoleConcatenatedView.php b/src/console/view/PhutilConsoleConcatenatedView.php new file mode 100644 --- /dev/null +++ b/src/console/view/PhutilConsoleConcatenatedView.php @@ -0,0 +1,22 @@ +items[] = $item; + return $this; + } + + protected function drawView() { + $output = array(); + + foreach ($this->items as $item) { + $output[] = $this->flattenView($item); + } + + return implode('', $output); + } + +} diff --git a/src/console/view/PhutilConsoleList.php b/src/console/view/PhutilConsoleList.php new file mode 100644 --- /dev/null +++ b/src/console/view/PhutilConsoleList.php @@ -0,0 +1,42 @@ +items[] = $item; + return $this; + } + + public function addItems(array $items) { + foreach ($items as $item) { + $this->addItem($item); + } + return $this; + } + + public function getItems() { + return $this->items; + } + + public function setWrap($wrap) { + $this->wrap = $wrap; + return $this; + } + + protected function drawView() { + $output = array(); + foreach ($this->getItems() as $item) { + if ($this->wrap) { + $item = phutil_console_wrap($item, 8); + } + $item = ' - '.$item; + $output[] = $item; + } + + return $this->drawLines($output); + } + +} diff --git a/src/console/PhutilConsoleTable.php b/src/console/view/PhutilConsoleTable.php rename from src/console/PhutilConsoleTable.php rename to src/console/view/PhutilConsoleTable.php --- a/src/console/PhutilConsoleTable.php +++ b/src/console/view/PhutilConsoleTable.php @@ -22,7 +22,7 @@ * ->setBorders(true) * ->draw(); */ -final class PhutilConsoleTable extends Phobject { +final class PhutilConsoleTable extends PhutilConsoleView { private $columns = array(); private $data = array(); @@ -30,30 +30,15 @@ private $borders = false; private $padding = 1; private $showHeader = true; - private $console; const ALIGN_LEFT = 'left'; const ALIGN_CENTER = 'center'; const ALIGN_RIGHT = 'right'; -/* -( Console )------------------------------------------------------------ */ - - protected function getConsole() { - if ($this->console) { - return $this->console; - } - return PhutilConsole::getConsole(); - } - - public function setConsole(PhutilConsole $console) { - $this->console = $console; - return $this; - } - - /* -( Configuration )------------------------------------------------------ */ + public function setBorders($borders) { $this->borders = $borders; return $this; @@ -103,19 +88,19 @@ /* -( Drawing )------------------------------------------------------------ */ - public function draw() { - $console = $this->getConsole(); - - $console->writeOut('%s', $this->getHeader()); - $console->writeOut('%s', $this->getBody()); - $console->writeOut('%s', $this->getFooter()); + protected function drawView() { + return $this->drawLines( + array_merge( + $this->getHeader(), + $this->getBody(), + $this->getFooter())); } private function getHeader() { - $output = ''; + $output = array(); if ($this->borders) { - $output .= $this->formatSeparator('='); + $output[] = $this->formatSeparator('='); } if (!$this->showHeader) { @@ -133,45 +118,45 @@ idx($column, 'align', self::ALIGN_LEFT)); } - $columns[] = PhutilConsoleFormatter::formatString( + $columns[] = tsprintf( '**%s**', $column_str); } - $output .= $this->formatRow($columns); + $output[] = $this->formatRow($columns); if ($this->borders) { - $output .= $this->formatSeparator('='); + $output[] = $this->formatSeparator('='); } return $output; } private function getBody() { - $output = ''; + $output = array(); foreach ($this->data as $data) { $columns = array(); foreach ($this->columns as $key => $column) { if (!$this->shouldAddSpacing($key, $column)) { - $columns[] = (string)idx($data, $key, ''); + $columns[] = idx($data, $key, ''); } else { $columns[] = $this->alignString( - (string)idx($data, $key, ''), + idx($data, $key, ''), $this->getWidth($key), idx($column, 'align', self::ALIGN_LEFT)); } } - $output .= $this->formatRow($columns); + $output[] = $this->formatRow($columns); } return $output; } private function getFooter() { - $output = ''; + $output = array(); if ($this->borders) { $columns = array(); @@ -180,7 +165,11 @@ $columns[] = str_repeat('=', $this->getWidth($column)); } - $output .= '+'.implode('+', $columns)."+\n"; + $output[] = array( + '+', + $this->implode('+', $columns), + '+', + ); } return $output; @@ -258,7 +247,11 @@ $left_padding = str_repeat(' ', $num_left_padding); $right_padding = str_repeat(' ', $num_right_padding); - return $left_padding.$string.$right_padding; + return array( + $left_padding, + $string, + $right_padding, + ); } /** @@ -272,9 +265,13 @@ if ($this->borders) { $separator = $padding.'|'.$padding; - return '|'.$padding.implode($separator, $columns).$padding."|\n"; + return array( + '|'.$padding, + $this->implode($separator, $columns), + $padding.'|', + ); } else { - return implode($padding, $columns)."\n"; + return $this->implode($padding, $columns); } } @@ -291,7 +288,11 @@ $columns[] = str_repeat($string, $this->getWidth($column)); } - return $separator.implode($separator, $columns).$separator."\n"; + return array( + $separator, + $this->implode($separator, $columns), + $separator, + ); } } diff --git a/src/console/view/PhutilConsoleView.php b/src/console/view/PhutilConsoleView.php new file mode 100644 --- /dev/null +++ b/src/console/view/PhutilConsoleView.php @@ -0,0 +1,112 @@ +console = $console; + return $this; + } + + final public function getConsole() { + if ($this->console) { + return $this->console; + } + return PhutilConsole::getConsole(); + } + + + /** + * Draw a view to the console. + * + * @return this + * @task draw + */ + final public function draw() { + $string = $this->drawConsoleString(); + + $console = $this->getConsole(); + $console->writeOut('%s', $string); + + return $this; + } + + + /** + * Draw a view to a string and return it. + * + * @return string Console-printable string. + * @task draw + */ + final public function drawConsoleString() { + $view = $this->drawView(); + $parts = $this->reduceView($view); + + $out = array(); + foreach ($parts as $part) { + $out[] = PhutilTerminalString::escapeStringValue($part, true); + } + + return implode('', $out); + } + + + /** + * Reduce a view to a list of simple, unnested parts. + * + * @param wild Any drawable view. + * @return list List of unnested drawables. + * @task draw + */ + private function reduceView($view) { + if ($view instanceof PhutilConsoleView) { + $view = $view->drawView(); + return $this->reduceView($view); + } + + if (is_array($view)) { + $parts = array(); + foreach ($view as $item) { + foreach ($this->reduceView($item) as $part) { + $parts[] = $part; + } + } + return $parts; + } + + return array($view); + } + +/* -( Drawing Utilities )-------------------------------------------------- */ + + + /** + * @param list List of views, one per line. + * @return wild Each view rendered on a separate line. + */ + final protected function drawLines(array $parts) { + $result = array(); + foreach ($parts as $part) { + if ($part !== null) { + $result[] = $part; + $result[] = "\n"; + } + } + + return $result; + } + + final protected function implode($separator, array $items) { + $result = array(); + foreach ($items as $item) { + $result[] = $item; + $result[] = $separator; + } + array_pop($result); + return $result; + } + +} diff --git a/src/xsprintf/tsprintf.php b/src/xsprintf/tsprintf.php --- a/src/xsprintf/tsprintf.php +++ b/src/xsprintf/tsprintf.php @@ -15,6 +15,7 @@ */ function tsprintf($pattern /* , ... */) { $args = func_get_args(); + $args[0] = PhutilConsoleFormatter::interpretFormat($args[0]); $string = xsprintf('xsprintf_terminal', null, $args); return new PhutilTerminalString($string); }