From 03b4efc50e217074e8aab5c852c80cdef5a22722 Mon Sep 17 00:00:00 2001 From: Aleksandr Pakhomov Date: Wed, 15 Mar 2023 20:02:36 +0400 Subject: [PATCH] Support ANSI colors and write documentation Now a user can define theirs own symbols for the progress bar. Also, ANSI colors are supported. The change is backward-compatible. --- docs/builder.md | 17 ++ .../DefaultProgressBarRenderer.java | 28 +-- .../me/tongfei/progressbar/DrawStyle.java | 42 +++++ .../tongfei/progressbar/DrawStyleBuilder.java | 168 ++++++++++++++++++ .../me/tongfei/progressbar/ProgressBar.java | 36 +++- .../progressbar/ProgressBarBuilder.java | 7 +- .../progressbar/CustomProgressBarTest.java | 97 ++++++++++ 7 files changed, 378 insertions(+), 17 deletions(-) create mode 100644 src/main/java/me/tongfei/progressbar/DrawStyle.java create mode 100644 src/main/java/me/tongfei/progressbar/DrawStyleBuilder.java create mode 100644 src/test/java/me/tongfei/progressbar/CustomProgressBarTest.java diff --git a/docs/builder.md b/docs/builder.md index d6f90a6..a3eee16 100644 --- a/docs/builder.md +++ b/docs/builder.md @@ -22,3 +22,20 @@ for (T x : ProgressBar.wrap(collection, pbb)) { ... } ``` + +Since `0.9.6` you can customize the progress bar style: + +``` java +ProgressBarBuilder pbb = new ProgressBarBuilder() + // ... + .setStyle(DrawStyle.builder() + // the color index from 0 to 255 (Ansi color table) + .colorCode(33) + .leftBracket("{") + .rightBracket("}") + .block('-') + .rightSideFractionSymbol('+') + .build() + ) + // ... +``` diff --git a/src/main/java/me/tongfei/progressbar/DefaultProgressBarRenderer.java b/src/main/java/me/tongfei/progressbar/DefaultProgressBarRenderer.java index 39e1468..e234916 100644 --- a/src/main/java/me/tongfei/progressbar/DefaultProgressBarRenderer.java +++ b/src/main/java/me/tongfei/progressbar/DefaultProgressBarRenderer.java @@ -17,7 +17,7 @@ */ public class DefaultProgressBarRenderer implements ProgressBarRenderer { - private ProgressBarStyle style; + private DrawStyle style; private String unitName; private long unitSize; private boolean isSpeedShown; @@ -27,7 +27,7 @@ public class DefaultProgressBarRenderer implements ProgressBarRenderer { private Function> eta; protected DefaultProgressBarRenderer( - ProgressBarStyle style, + DrawStyle style, String unitName, long unitSize, boolean isSpeedShown, @@ -53,7 +53,7 @@ protected int progressIntegralPart(ProgressState progress, int length) { protected int progressFractionalPart(ProgressState progress, int length) { double p = progress.getNormalizedProgress() * length; - double fraction = (p - Math.floor(p)) * style.fractionSymbols.length(); + double fraction = (p - Math.floor(p)) * style.fractionSymbols().length(); return (int) Math.floor(fraction); } @@ -112,7 +112,7 @@ public String render(ProgressState progress, int maxLength) { return ""; } - String prefix = progress.getTaskName() + " " + percentage(progress) + " " + style.leftBracket; + String prefix = progress.getTaskName() + " " + percentage(progress) + " " + style.leftBracket(); int prefixLength = getStringDisplayLength(prefix); if (prefixLength > maxLength) { @@ -124,7 +124,7 @@ public String render(ProgressState progress, int maxLength) { int maxSuffixLength = Math.max(maxLength - prefixLength - 1, 0); String speedString = isSpeedShown ? speed(progress) : ""; - String suffix = style.rightBracket + " " + ratio(progress) + " (" + String suffix = style.rightBracket() + " " + ratio(progress) + " (" + Util.formatDuration(progress.getTotalElapsed()) + (isEtaShown ? " / " + etaString(progress) : "") + ") " @@ -144,24 +144,24 @@ public String render(ProgressState progress, int maxLength) { // case of indefinite progress bars if (progress.indefinite) { int pos = (int)(progress.current % length); - sb.append(Util.repeat(style.space, pos)); - sb.append(style.block); - sb.append(Util.repeat(style.space, length - pos - 1)); + sb.append(Util.repeat(style.space(), pos)); + sb.append(style.block()); + sb.append(Util.repeat(style.space(), length - pos - 1)); } // case of definite progress bars else { - sb.append(Util.repeat(style.block, progressIntegralPart(progress, length))); + sb.append(Util.repeat(style.block(), progressIntegralPart(progress, length))); if (progress.current < progress.max) { int fraction = progressFractionalPart(progress, length); if (fraction != 0) { - sb.append(style.fractionSymbols.charAt(fraction)); - sb.append(style.delimitingSequence); + sb.append(style.fractionSymbols().charAt(fraction)); + sb.append(style.delimitingSequence()); } else { - sb.append(style.delimitingSequence); - sb.append(style.rightSideFractionSymbol); + sb.append(style.delimitingSequence()); + sb.append(style.rightSideFractionSymbol()); } - sb.append(Util.repeat(style.space, length - progressIntegralPart(progress, length) - 1)); + sb.append(Util.repeat(style.space(), length - progressIntegralPart(progress, length) - 1)); } } diff --git a/src/main/java/me/tongfei/progressbar/DrawStyle.java b/src/main/java/me/tongfei/progressbar/DrawStyle.java new file mode 100644 index 0000000..4d48438 --- /dev/null +++ b/src/main/java/me/tongfei/progressbar/DrawStyle.java @@ -0,0 +1,42 @@ +package me.tongfei.progressbar; + +/** + * A draw style for a progress bar. + * + * @author Aleksandr Pakhomov + */ +public interface DrawStyle { + /** Create draw style object from {@link ProgressBarStyle} */ + static DrawStyle from(ProgressBarStyle style) { + return builder().apply(style).build(); + } + + /** Symbol to refresh the progress bar. */ + String refreshPrompt(); + + /** Left bracket of the progress bar. */ + String leftBracket(); + + /** Delimiting sequence between the progress bar and the status text. */ + String delimitingSequence(); + + /** Right bracket of the progress bar. */ + String rightBracket(); + + /** Block character of the progress bar. [=====> ] here '=' is the block. */ + char block(); + + /** Space character of the progress bar. [=====> ] here ' ' is the space. */ + char space(); + + /** Fraction symbols of the progress bar. */ + String fractionSymbols(); + + /** Right side fraction symbol of the progress bar. [=====> ] here '>'. */ + char rightSideFractionSymbol(); + + /** Create a new builder for {@link DrawStyle} */ + static DrawStyleBuilder builder() { + return new DrawStyleBuilder(); + } +} diff --git a/src/main/java/me/tongfei/progressbar/DrawStyleBuilder.java b/src/main/java/me/tongfei/progressbar/DrawStyleBuilder.java new file mode 100644 index 0000000..291853e --- /dev/null +++ b/src/main/java/me/tongfei/progressbar/DrawStyleBuilder.java @@ -0,0 +1,168 @@ +package me.tongfei.progressbar; + +/** + * Builder for {@link DrawStyle}. + * + * @author Aleksandr Pakhomov + */ +public class DrawStyleBuilder { + private static final String ESC_CODE = "\u001b["; + + private String refreshPrompt = "\r"; + private String leftBracket = "["; + private String delimitingSequence = ""; + private String rightBracket = "]"; + private char block = '='; + private char space = ' '; + private String fractionSymbols = ">"; + private char rightSideFractionSymbol = '>'; + private int colorCode = 0; + + /** Set refresh prompt. Default "\r". */ + public DrawStyleBuilder refreshPrompt(String refreshPrompt) { + this.refreshPrompt = refreshPrompt; + return this; + } + + /** Set left bracket. Default "[". */ + public DrawStyleBuilder leftBracket(String leftBracket) { + this.leftBracket = leftBracket; + return this; + } + + /** Set delimiting sequence. Default "". */ + public DrawStyleBuilder delimitingSequence(String delimitingSequence) { + this.delimitingSequence = delimitingSequence; + return this; + } + + /** Set right bracket. Default "]". */ + public DrawStyleBuilder rightBracket(String rightBracket) { + this.rightBracket = rightBracket; + return this; + } + + /** Set block character. Default "=" */ + public DrawStyleBuilder block(char block) { + this.block = block; + return this; + } + + /** Set space character. Default " " */ + public DrawStyleBuilder space(char space) { + this.space = space; + return this; + } + + /** Set fraction symbols. Default ">" */ + public DrawStyleBuilder fractionSymbols(String fractionSymbols) { + this.fractionSymbols = fractionSymbols; + return this; + } + + /** Set right side fraction symbol. Default ">" */ + public DrawStyleBuilder rightSideFractionSymbol(char rightSideFractionSymbol) { + this.rightSideFractionSymbol = rightSideFractionSymbol; + return this; + } + + /** Set ANSI color code. Default 0 (no color). Must be in [0, 255]. */ + public DrawStyleBuilder colorCode(int code) { + if (code < 0 || code > 255) + throw new IllegalArgumentException("Color code must be between 0 and 255."); + + this.colorCode = code; + return this; + } + + /** Build {@link DrawStyle}. */ + public DrawStyle build() { + boolean colorDefined = colorCode != 0; + + if (colorDefined && leftBracket.contains(ESC_CODE)) { + throw new IllegalArgumentException("The color code is overridden with left bracket escape code. " + + "Please, remove the escape sequence from the left bracket or do not use color code."); + } + + String prefix = colorDefined ? (ESC_CODE + colorCode + "m") : ""; + String postfix = colorDefined ? ESC_CODE + "0m" : ""; + + return new InternalDrawStyle(refreshPrompt, prefix + leftBracket, delimitingSequence, + rightBracket + postfix, block, space, fractionSymbols, rightSideFractionSymbol); + } + + /** Apply {@link ProgressBarStyle}. */ + public DrawStyleBuilder apply(ProgressBarStyle style) { + refreshPrompt(style.refreshPrompt); + leftBracket(style.leftBracket); + delimitingSequence(style.delimitingSequence); + rightBracket(style.rightBracket); + block(style.block); + space(style.space); + fractionSymbols(style.fractionSymbols); + return rightSideFractionSymbol(style.rightSideFractionSymbol); + } + + static class InternalDrawStyle implements DrawStyle { + private final String refreshPrompt; + private final String leftBracket; + private final String delimitingSequence; + private final String rightBracket; + private final char block; + private final char space; + private final String fractionSymbols; + private final char rightSideFractionSymbol; + + private InternalDrawStyle(String refreshPrompt, String leftBracket, String delimitingSequence, + String rightBracket, char block, char space, String fractionSymbols, char rightSideFractionSymbol) { + this.refreshPrompt = refreshPrompt; + this.leftBracket = leftBracket; + this.delimitingSequence = delimitingSequence; + this.rightBracket = rightBracket; + this.block = block; + this.space = space; + this.fractionSymbols = fractionSymbols; + this.rightSideFractionSymbol = rightSideFractionSymbol; + } + + @Override + public String refreshPrompt() { + return refreshPrompt; + } + + @Override + public String leftBracket() { + return leftBracket; + } + + @Override + public String delimitingSequence() { + return delimitingSequence; + } + + @Override + public String rightBracket() { + return rightBracket; + } + + @Override + public char block() { + return block; + } + + @Override + public char space() { + return space; + } + + @Override + public String fractionSymbols() { + return fractionSymbols; + } + + @Override + public char rightSideFractionSymbol() { + return rightSideFractionSymbol; + } + } +} diff --git a/src/main/java/me/tongfei/progressbar/ProgressBar.java b/src/main/java/me/tongfei/progressbar/ProgressBar.java index ba70c34..f5f4a7b 100644 --- a/src/main/java/me/tongfei/progressbar/ProgressBar.java +++ b/src/main/java/me/tongfei/progressbar/ProgressBar.java @@ -50,7 +50,7 @@ public ProgressBar(String task, long initialMax) { * @param initialMax Initial maximum value * @param updateIntervalMillis Update interval (default value 1000 ms) * @param continuousUpdate Rerender every time the update interval happens regardless of progress count. - * @param style Output style (default value ProgressBarStyle.UNICODE_BLOCK) + * @param style Draw style * @param showSpeed Should the calculated speed be displayed * @param speedFormat Speed number format * @deprecated Use {@link ProgressBarBuilder} instead. @@ -62,7 +62,7 @@ public ProgressBar( boolean continuousUpdate, boolean clearDisplayOnFinish, PrintStream os, - ProgressBarStyle style, + DrawStyle style, String unitName, long unitSize, boolean showSpeed, @@ -81,6 +81,38 @@ public ProgressBar( ); } + /** + * Creates a progress bar with the specific taskName name, initial maximum value, + * customized update interval (default 1000 ms), the PrintStream to be used, and output style. + * @param task Task name + * @param initialMax Initial maximum value + * @param updateIntervalMillis Update interval (default value 1000 ms) + * @param continuousUpdate Rerender every time the update interval happens regardless of progress count. + * @param style Output style (default value ProgressBarStyle.UNICODE_BLOCK) + * @param showSpeed Should the calculated speed be displayed + * @param speedFormat Speed number format + * @deprecated Use {@link ProgressBarBuilder} instead. + */ + public ProgressBar( + String task, + long initialMax, + int updateIntervalMillis, + boolean continuousUpdate, + boolean clearDisplayOnFinish, + PrintStream os, + ProgressBarStyle style, + String unitName, + long unitSize, + boolean showSpeed, + DecimalFormat speedFormat, + ChronoUnit speedUnit, + long processed, + Duration elapsed + ) { + this(task, initialMax, updateIntervalMillis, continuousUpdate, clearDisplayOnFinish, os, + DrawStyle.from(style), unitName, unitSize, showSpeed, speedFormat, speedUnit, processed, elapsed); + } + /** * Creates a progress bar with the specific name, initial maximum value, customized update interval (default 1s), * and the provided progress bar renderer ({@link ProgressBarRenderer}) and consumer ({@link ProgressBarConsumer}). diff --git a/src/main/java/me/tongfei/progressbar/ProgressBarBuilder.java b/src/main/java/me/tongfei/progressbar/ProgressBarBuilder.java index 6dabdf8..3b97d12 100644 --- a/src/main/java/me/tongfei/progressbar/ProgressBarBuilder.java +++ b/src/main/java/me/tongfei/progressbar/ProgressBarBuilder.java @@ -17,7 +17,7 @@ public class ProgressBarBuilder { private long initialMax = -1; private int updateIntervalMillis = 1000; private boolean continuousUpdate = false; - private ProgressBarStyle style = ProgressBarStyle.COLORFUL_UNICODE_BLOCK; + private DrawStyle style = DrawStyle.from(ProgressBarStyle.COLORFUL_UNICODE_BLOCK); private ProgressBarConsumer consumer = null; private boolean clearDisplayOnFinish = false; private String unitName = ""; @@ -48,6 +48,11 @@ public ProgressBarBuilder setInitialMax(long initialMax) { } public ProgressBarBuilder setStyle(ProgressBarStyle style) { + this.style = DrawStyle.from(style); + return this; + } + + public ProgressBarBuilder setStyle(DrawStyle style) { this.style = style; return this; } diff --git a/src/test/java/me/tongfei/progressbar/CustomProgressBarTest.java b/src/test/java/me/tongfei/progressbar/CustomProgressBarTest.java new file mode 100644 index 0000000..bcd59d2 --- /dev/null +++ b/src/test/java/me/tongfei/progressbar/CustomProgressBarTest.java @@ -0,0 +1,97 @@ +package me.tongfei.progressbar; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** @author Aleksandr Pakhomov */ +public class CustomProgressBarTest { + + ProgressBarBuilder builder; + + private static void simulateProgress(ProgressBar bar) throws InterruptedException { + int x = 0; + while (x < 10000) { + bar.step(); + Thread.sleep(1); + x++; + } + } + + @BeforeEach + void setUp() { + builder = new ProgressBarBuilder() + .setUnit("k", 1000) + .setInitialMax(10000); + } + + @Test + void defaultProgressBarStyle() throws InterruptedException { + // Given default + try (ProgressBar bar = builder.build()) { + // Expect display default progress bar style: + // 100% │███████████████████████████████████████████│ 10/10k (0:00:12 / 0:00:00) + simulateProgress(bar); + } + } + + @Test + void customPredefinedProgressBarStyle() throws InterruptedException { + // Given ASCII progress bar style that is taken from ProgressBarStyle enum + builder.setStyle(ProgressBarStyle.UNICODE_BLOCK); + try (ProgressBar bar = builder.build()) { + // Expect display custom progress bar style: + // 50% [=================> ] 5/10k (0:00:06 / 0:00:12) + simulateProgress(bar); + } + } + + @Test + void customUserDefinedProgressBarStyleWithColor() throws InterruptedException { + // Given custom progress bar style + builder.setStyle( + DrawStyle.builder() + .colorCode(36) + .leftBracket("{") + .rightBracket("}") + .block('-') + .rightSideFractionSymbol('+') + .build() + ); + try (ProgressBar bar = builder.build()) { + // Expect display custom progress bar style: + // 50% {-------------------> } 5/10k (0:00:06 / 0:00:12) + simulateProgress(bar); + } + } + + @Test + void customColorCannotBeUsedWithEscapeSymbol() { + // Given draw style with both color code and escape symbols + DrawStyleBuilder drawStyleBuilder = DrawStyle.builder() + .colorCode(33) // yellow + .leftBracket("\u001b[36m{");// but this overrides color code + + // Expect + assertThrows(IllegalArgumentException.class, drawStyleBuilder::build); + } + + @Test + void customColorLessThen0() { + // Expect color code cannot be less than 0 + assertThrows(IllegalArgumentException.class, () -> DrawStyle.builder().colorCode(-1)); + + // But color code can be 0 + assertDoesNotThrow(DrawStyle.builder().colorCode(0)::build); + } + + @Test + void customColorMoreThen255() { + // Expect color code cannot be more than 255 + assertThrows(IllegalArgumentException.class, () -> DrawStyle.builder().colorCode(256)); + // But color code can be 255 + assertDoesNotThrow(DrawStyle.builder().colorCode(255)::build); + } +}