Skip to content

Commit

Permalink
Fix text width calculation for ANSI formatted help output
Browse files Browse the repository at this point in the history
ANSI escape sequences have to be ignored to get the correct
width.

Target: trunk
Require-notes: no
Require-book: no
Acked-by: Paul Millar <paul.millar@desy.de>
Patch: http://rb.dcache.org/r/6412/
  • Loading branch information
gbehrmann committed Jan 21, 2014
1 parent 548b541 commit 1750f0b
Show file tree
Hide file tree
Showing 4 changed files with 129 additions and 28 deletions.
Expand Up @@ -19,11 +19,19 @@

import jline.ANSIBuffer;

import org.dcache.commons.util.Strings;

/**
* Utility class to produce help texts suitable for an ANSI terminal.
*/
public class AnsiHelpPrinter extends TextHelpPrinter
{
@Override
protected int plainLength(String s)
{
return Strings.plainLength(s);
}

@Override
protected String value(String value)
{
Expand Down
Expand Up @@ -216,7 +216,7 @@ public String getHelpHint(Command command, Class<?> clazz)
{
String hint = (command.hint().isEmpty() ? "" : "# " + command.hint());
String signature = getSignature(clazz);
if (signature.length() + hint.length() > 78) {
if (plainLength(signature) + plainLength(hint) > 78) {
signature = getShortSignature(clazz);
}
return (signature.isEmpty() ? "" : signature + " ") + hint;
Expand Down Expand Up @@ -286,6 +286,11 @@ public String getHelp(Command command, Class<?> clazz)
return out.toString();
}

protected int plainLength(String s)
{
return s.length();
}

protected abstract String value(String value);

protected abstract String literal(String option);
Expand Down
96 changes: 69 additions & 27 deletions modules/common/src/main/java/org/dcache/commons/util/Strings.java
Expand Up @@ -25,6 +25,7 @@ public final class Strings {
private static final Logger LOGGER =
LoggerFactory.getLogger( Strings.class);

private static final String ANSI_ESCAPE = "\u001b[";
private static final String[] ZERO_LENGTH_STRING_ARRAY=new String[0];
private static final String INFINITY = "infinity";

Expand Down Expand Up @@ -81,9 +82,59 @@ public static String[] splitArgumentString(String argumentString) {
return matchList.toArray(new String[matchList.size()]);
}

public static int plainLength(String s)
{
int length = s.length();
int plainLength = length;
int i = s.indexOf(ANSI_ESCAPE);
while (i > -1) {
plainLength -= ANSI_ESCAPE.length();
i += ANSI_ESCAPE.length();
if (i < length) {
while (i + 1 < length && (s.charAt(i) < 64 || s.charAt(i) > 126)) {
i++;
plainLength--;
}
i++;
plainLength--;
}
i = s.indexOf(ANSI_ESCAPE, i);
}
return plainLength;
}

/**
* Locates the last occurrence of a white space character after fromIndex and before
* wrapLength characters, or the first occurrence of a white space character after
* fromIndex if there is no white space before wrapLength characters.
*
* ANSI escape sequences are considered to have zero width.
*/
private static int indexOfNextWrap(char[] chars, int fromIndex, int wrapLength)
{
int lastWrap = -1;
int max = fromIndex + wrapLength;
int length = chars.length;
for (int i = fromIndex; i < length && (i <= max || lastWrap == -1); i++) {
if (Character.isWhitespace(chars[i])) {
lastWrap = i;
} else if (chars[i] == 27 && i < length && chars[i + 1] == '[') {
i += 2;
max += 2;
for (;i < length && (chars[i] < 64 || chars[i] > 126); i++) {
max++;
}
max++;
}
}
return length <= max || lastWrap == -1 ? length : lastWrap;
}

/**
* Wraps a text to a particular width.
*
* ANSI escape sequences are considered to have zero width.
*
* @param indent String to place at the beginning of each line
* @param str String to wrap
* @param wrapLength Width to wrap to excluding indent
Expand All @@ -92,39 +143,30 @@ public static String[] splitArgumentString(String argumentString) {
public static String wrap(String indent, String str, int wrapLength)
{
int offset = 0;
StringBuilder out = new StringBuilder();

while (offset < str.length()) {
int eop = str.indexOf('\n', offset);
if (eop < 0) {
eop = str.length();
StringBuilder out = new StringBuilder(str.length());

char[] chars = str.toCharArray();
int length = chars.length;
while (offset < length) {
int eop = offset;
while (eop < length && chars[eop] != '\n') {
eop++;
}

boolean firstLine = true;
while ((eop - offset) > wrapLength) {
if (!firstLine && str.charAt(offset) == ' ') {
offset++;
continue;
}

int spaceToWrapAt = str.lastIndexOf(' ', wrapLength + offset);
// if the next string with length wrapLength doesn't contain ' '
if (spaceToWrapAt < offset) {
spaceToWrapAt = str.indexOf(' ', wrapLength + offset);
// if no more ' '
if (spaceToWrapAt < 0 || spaceToWrapAt > eop) {
break;
}
}

int spaceToWrapAt;
while ((spaceToWrapAt = indexOfNextWrap(chars, offset, wrapLength)) < eop) {
out.append(indent);
out.append(str.substring(offset, spaceToWrapAt));
out.append("\n");
out.append(chars, offset, spaceToWrapAt - offset);
out.append('\n');
offset = spaceToWrapAt + 1;
firstLine = false;

// Skip leading spaces on next line
while (offset < length && chars[offset] == ' ') {
offset++;
}
}

out.append(indent).append(str.substring(offset, eop)).append('\n');
out.append(indent).append(chars, offset, eop - offset).append('\n');
offset = eop + 1;
}

Expand Down
Expand Up @@ -2,7 +2,11 @@

import org.junit.Test;

import static org.dcache.commons.util.Strings.plainLength;
import static org.dcache.commons.util.Strings.wrap;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertThat;

/**
*
Expand Down Expand Up @@ -106,4 +110,46 @@ public void testString9Split() {
assertArrayEquals(splitString, testString9ExpectedSplit);
}

@Test
public void testPlainLength() {
assertThat(plainLength(""), is(0));
assertThat(plainLength("1"), is(1));
assertThat(plainLength("12"), is(2));
assertThat(plainLength("\u001b["), is(0));
assertThat(plainLength("\u001b[m"), is(0));
assertThat(plainLength("\u001b[1m"), is(0));
assertThat(plainLength("\u001b[12m"), is(0));
assertThat(plainLength("foo\u001b["), is(3));
assertThat(plainLength("foo\u001b[m"), is(3));
assertThat(plainLength("foo\u001b[1m"), is(3));
assertThat(plainLength("foo\u001b[12m"), is(3));
assertThat(plainLength("foo\u001b[m" + "bar"), is(6));
assertThat(plainLength("foo\u001b[1m" + "bar"), is(6));
assertThat(plainLength("foo\u001b[12m" + "bar"), is(6));
}

@Test
public void testWrap() {
assertThat(wrap("", "The quick brown fox jumps over the lazy dog.", 70),
is("The quick brown fox jumps over the lazy dog.\n"));
assertThat(wrap(" ", "The quick brown fox jumps over the lazy dog.", 70),
is(" The quick brown fox jumps over the lazy dog.\n"));
assertThat(wrap(" ", "The quick brown fox jumps\nover the lazy dog.", 70),
is(" The quick brown fox jumps\n over the lazy dog.\n"));
assertThat(wrap(" ", "The quick brown fox jumps over the lazy dog.", 14),
is(" The quick\n brown fox\n jumps over the\n lazy dog.\n"));
assertThat(wrap(" ", "The quick brown fox jumps over the lazy dog.", 15),
is(" The quick brown\n fox jumps over\n the lazy dog.\n"));
assertThat(wrap(" ", "The quick brown fox jumps over the lazy dog.", 16),
is(" The quick brown\n fox jumps over\n the lazy dog.\n"));
assertThat(wrap(" ", " The quick brown fox jumps over the lazy dog.", 16),
is(" The quick\n brown fox jumps\n over the lazy\n dog.\n"));
assertThat(wrap(" ", "\u001B[1mThe quick brown\u001B[1m \u001B[1mfox jumps over the lazy dog.\u001B[1m", 15),
is(" \u001B[1mThe quick brown\u001B[1m\n \u001B[1mfox jumps over\n the lazy dog.\u001B[1m\n"));
assertThat(
wrap(" ", "\u001B[1mThe quick brown\u001B[1m \u001B[1mfox jumps over the lazy dog.\u001B[1m\n\n"
+ "\u001B[1mThe quick brown\u001B[1m \u001B[1mfox jumps over the lazy dog.\u001B[1m", 15),
is(" \u001B[1mThe quick brown\u001B[1m\n \u001B[1mfox jumps over\n the lazy dog.\u001B[1m\n \n"
+ " \u001B[1mThe quick brown\u001B[1m\n \u001B[1mfox jumps over\n the lazy dog.\u001B[1m\n"));
}
}

0 comments on commit 1750f0b

Please sign in to comment.