diff --git a/src/console/format.php b/src/console/format.php index 6689878..3e30d5a 100644 --- a/src/console/format.php +++ b/src/console/format.php @@ -1,217 +1,224 @@ /dev/null; '. 'read -e -p %s; '. 'echo "$REPLY"; '. 'history -s "$REPLY" 2>/dev/null; '. 'history -w %s 2>/dev/null', $history, $prompt, $history)); // execx() doesn't work with input, phutil_passthru() doesn't return output. $response = shell_exec($command); } return rtrim($response, "\r\n"); } /** * Soft wrap text for display on a console, respecting UTF8 character boundaries * and ANSI color escape sequences. * * @param string Text to wrap. * @param int Optional indent level. + * @param bool True to also indent the first line. * @return string Wrapped text. */ -function phutil_console_wrap($text, $indent = 0) { +function phutil_console_wrap($text, $indent = 0, $with_prefix = true) { $lines = array(); $width = (78 - $indent); $esc = chr(27); $break_pos = null; $len_after_break = 0; $line_len = 0; $line = array(); $lines = array(); $vector = phutil_utf8v($text); $vector_len = count($vector); for ($ii = 0; $ii < $vector_len; $ii++) { $chr = $vector[$ii]; // If this is an ANSI escape sequence for a color code, just consume it // without counting it toward the character limit. This prevents lines // with bold/color on them from wrapping too early. if ($chr == $esc) { for ($ii; $ii < $vector_len; $ii++) { $line[] = $vector[$ii]; if ($vector[$ii] == 'm') { break; } } continue; } $line[] = $chr; ++$line_len; ++$len_after_break; if ($line_len > $width) { if ($break_pos !== null) { $slice = array_slice($line, 0, $break_pos); while (count($slice) && end($slice) == ' ') { array_pop($slice); } $slice[] = "\n"; $lines[] = $slice; $line = array_slice($line, $break_pos); $line_len = $len_after_break; $len_after_break = 0; $break_pos = null; } } if ($chr == ' ') { $break_pos = count($line); $len_after_break = 0; } if ($chr == "\n") { $lines[] = $line; $line = array(); $len_after_break = 0; $line_len = 0; $break_pos = null; } } if ($line) { if ($line) { $lines[] = $line; } } $pre = null; if ($indent) { $pre = str_repeat(' ', $indent); } foreach ($lines as $idx => $line) { - $lines[$idx] = $pre.implode('', $line); + if ($idx == 0 && !$with_prefix) { + $prefix = null; + } else { + $prefix = $pre; + } + + $lines[$idx] = $prefix.implode('', $line); } return implode('', $lines); } function phutil_console_require_tty() { if (function_exists('posix_isatty') && !posix_isatty(STDIN)) { throw new PhutilConsoleStdinNotInteractiveException(); } } /** * Determine the width of the terminal, if possible. Returns `null` on failure. * * @return int|null Terminal width in characters, or null on failure. */ function phutil_console_get_terminal_width() { static $width; if ($width === null) { if (phutil_is_windows()) { // TODO: Figure out how to get this working in Windows. return null; } $tmp = new TempFile(); // NOTE: We can't just execute this because it won't be connected to a TTY // if we do. $err = phutil_passthru('tput cols > %s', $tmp); if ($err) { return null; } try { $cols = Filesystem::readFile($tmp); } catch (FilesystemException $ex) { return null; } $width = (int)$cols; if (!$width) { $width = null; } } return $width; } diff --git a/src/console/view/PhutilConsoleBlock.php b/src/console/view/PhutilConsoleBlock.php index f62c1c8..ae60270 100644 --- a/src/console/view/PhutilConsoleBlock.php +++ b/src/console/view/PhutilConsoleBlock.php @@ -1,45 +1,48 @@ 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"; + $item = array( + tsprintf('%s', $item)->applyWrap(), + "\n", + ); break; case 'list': $item = $item; break; } $output[] = $item; } return $this->drawLines($output); } } diff --git a/src/console/view/PhutilConsoleList.php b/src/console/view/PhutilConsoleList.php index 6b69eff..ffccfa7 100644 --- a/src/console/view/PhutilConsoleList.php +++ b/src/console/view/PhutilConsoleList.php @@ -1,62 +1,63 @@ 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 setBullet($bullet) { $this->bullet = $bullet; return $this; } public function getBullet() { return $this->bullet; } public function setWrap($wrap) { $this->wrap = $wrap; return $this; } protected function drawView() { $indent_depth = 6; $indent_string = str_repeat(' ', $indent_depth); if ($this->bullet !== null) { $bullet = $this->bullet.' '; $indent_depth = $indent_depth + phutil_utf8_console_strlen($bullet); } else { $bullet = ''; } $output = array(); foreach ($this->getItems() as $item) { if ($this->wrap) { - $item = phutil_console_wrap($item, $indent_depth); + $item = tsprintf('%s', $item) + ->applyIndent($indent_depth, false); } $output[] = $indent_string.$bullet.$item; } return $this->drawLines($output); } } diff --git a/src/xsprintf/PhutilTerminalString.php b/src/xsprintf/PhutilTerminalString.php index 3264a6e..8d99b09 100644 --- a/src/xsprintf/PhutilTerminalString.php +++ b/src/xsprintf/PhutilTerminalString.php @@ -1,63 +1,75 @@ string = $string; } public function __toString() { return $this->string; } + public function applyWrap() { + $string = (string)$this; + $string = phutil_console_wrap($string); + return new self($string); + } + + public function applyIndent($depth, $with_prefix = true) { + $string = (string)$this; + $string = phutil_console_wrap($string, $depth, $with_prefix); + return new self($string); + } + public static function escapeStringValue($value, $allow_whitespace) { if ($value instanceof PhutilTerminalString) { return (string)$value; } $value = (string)$value; static $escape_map; if ($escape_map === null) { $escape_map = array( chr(0x00) => '', chr(0x07) => '', chr(0x08) => '', chr(0x09) => '', chr(0x0A) => '', chr(0x0D) => '', chr(0x1B) => '', chr(0x7F) => '', ); for ($ii = 0; $ii < 32; $ii++) { $c = chr($ii); if (empty($escape_map[$c])) { $escape_map[$c] = sprintf('<0x%02X>', $ii); } } } $map = $escape_map; if ($allow_whitespace) { unset($map["\r"]); unset($map["\n"]); unset($map["\t"]); } $value = str_replace(array_keys($map), array_values($map), $value); // In this mode, we additionally escape any which is not immediately // followed by . if ($allow_whitespace) { $value = preg_replace('/\r(?!\n)/', '', $value); } return $value; } }