diff --git a/twinkle-ansi/src/main/java/org/codejive/twinkle/ansi/Ansi.java b/twinkle-ansi/src/main/java/org/codejive/twinkle/ansi/Ansi.java index a00b59b..8cb2b35 100644 --- a/twinkle-ansi/src/main/java/org/codejive/twinkle/ansi/Ansi.java +++ b/twinkle-ansi/src/main/java/org/codejive/twinkle/ansi/Ansi.java @@ -1,5 +1,7 @@ package org.codejive.twinkle.ansi; +import java.io.IOException; + public class Ansi { public static final char ESC = '\u001B'; @@ -68,22 +70,26 @@ public static String style(Object... styles) { if (styles == null || styles.length == 0) { return ""; } - return style(new StringBuilder(), styles).toString(); + try { + return style(new StringBuilder(), styles).toString(); + } catch (IOException e) { + throw new RuntimeException(e); + } } - public static StringBuilder style(StringBuilder sb, Object... styles) { + public static Appendable style(Appendable appendable, Object... styles) throws IOException { if (styles == null || styles.length == 0) { - return sb; + return appendable; } - sb.append(CSI); + appendable.append(CSI); for (int i = 0; i < styles.length; i++) { - sb.append(styles[i]); + appendable.append(styles[i].toString()); if (i < styles.length - 1) { - sb.append(";"); + appendable.append(";"); } } - sb.append("m"); - return sb; + appendable.append("m"); + return appendable; } public static String foreground(int index) { diff --git a/twinkle-ansi/src/main/java/org/codejive/twinkle/ansi/Printable.java b/twinkle-ansi/src/main/java/org/codejive/twinkle/ansi/Printable.java new file mode 100644 index 0000000..dd2c3fb --- /dev/null +++ b/twinkle-ansi/src/main/java/org/codejive/twinkle/ansi/Printable.java @@ -0,0 +1,70 @@ +package org.codejive.twinkle.ansi; + +import java.io.IOException; +import org.jspecify.annotations.NonNull; + +public interface Printable { + /** + * Converts the object to an ANSI string, including ANSI escape codes for styles. This method + * resets the current style to default at the start of the string. + * + * @return The ANSI string representation of the object. + */ + @NonNull String toAnsiString(); + + /** + * Outputs the object as an ANSI string, including ANSI escape codes for styles. This method + * resets the current style to default at the start of the output. + * + * @param appendable The Appendable to write the ANSI output to. + * @return The Appendable passed as parameter. + */ + @NonNull Appendable toAnsi(Appendable appendable) throws IOException; + + /** + * Converts the object to an ANSI string, including ANSI escape codes for styles. This method + * takes into account the provided current style to generate a result that is as efficient as + * possible in terms of ANSI codes. + * + * @param currentStyle The current style to start with. + * @return The ANSI string representation of the object. + */ + default @NonNull String toAnsiString(Style currentStyle) { + return toAnsiString(currentStyle.state()); + } + + /** + * Outputs the object as an ANSI string, including ANSI escape codes for styles. This method + * takes into account the provided current style to generate a result that is as efficient as + * possible in terms of ANSI codes. + * + * @param appendable The Appendable to write the ANSI output to. + * @param currentStyle The current style to start with. + * @return The Appendable passed as parameter. + */ + default @NonNull Appendable toAnsi(Appendable appendable, Style currentStyle) + throws IOException { + return toAnsi(appendable, currentStyle.state()); + } + + /** + * Converts the object to an ANSI string, including ANSI escape codes for styles. This method + * takes into account the provided current style to generate a result that is as efficient as + * possible in terms of ANSI codes. + * + * @param currentStyleState The current style to start with. + * @return The ANSI string representation of the object. + */ + @NonNull String toAnsiString(long currentStyleState); + + /** + * Outputs the object as an ANSI string, including ANSI escape codes for styles. This method + * takes into account the provided current style to generate a result that is as efficient as + * possible in terms of ANSI codes. + * + * @param appendable The Appendable to write the ANSI output to. + * @param currentStyleState The current style to start with. + * @return The Appendable passed as parameter. + */ + @NonNull Appendable toAnsi(Appendable appendable, long currentStyleState) throws IOException; +} diff --git a/twinkle-ansi/src/main/java/org/codejive/twinkle/ansi/Style.java b/twinkle-ansi/src/main/java/org/codejive/twinkle/ansi/Style.java index 843eb0f..80a78f8 100644 --- a/twinkle-ansi/src/main/java/org/codejive/twinkle/ansi/Style.java +++ b/twinkle-ansi/src/main/java/org/codejive/twinkle/ansi/Style.java @@ -1,10 +1,11 @@ package org.codejive.twinkle.ansi; +import java.io.IOException; import java.util.ArrayList; import java.util.List; import org.jspecify.annotations.NonNull; -public class Style { +public class Style implements Printable { private final long state; private static final long IDX_BOLD = 0; @@ -347,27 +348,32 @@ public String toString() { return sb.toString(); } - public String toAnsiString() { + @Override + public @NonNull String toAnsiString() { return toAnsiString(UNSTYLED); } - public StringBuilder toAnsiString(StringBuilder sb) { - return toAnsiString(sb, Style.UNSTYLED); - } - - public String toAnsiString(Style currentStyle) { - return toAnsiString(currentStyle.state()); - } - - public StringBuilder toAnsiString(StringBuilder sb, Style currentStyle) { - return toAnsiString(sb, currentStyle.state()); + @Override + public @NonNull Appendable toAnsi(Appendable appendable) throws IOException { + try { + return toAnsi(appendable, Style.UNSTYLED); + } catch (IOException e) { + throw new RuntimeException(e); + } } - public String toAnsiString(long currentStyleState) { - return toAnsiString(new StringBuilder(), currentStyleState).toString(); + @Override + public @NonNull String toAnsiString(long currentStyleState) { + try { + return toAnsi(new StringBuilder(), currentStyleState).toString(); + } catch (IOException e) { + throw new RuntimeException(e); + } } - public StringBuilder toAnsiString(StringBuilder sb, long currentStyleState) { + @Override + public @NonNull Appendable toAnsi(Appendable appendable, long currentStyleState) + throws IOException { List styles = new ArrayList<>(); if ((currentStyleState & (F_BOLD | F_FAINT)) != (state & (F_BOLD | F_FAINT))) { // First we switch to NORMAL to clear both BOLD and FAINT @@ -426,6 +432,6 @@ public StringBuilder toAnsiString(StringBuilder sb, long currentStyleState) { if ((currentStyleState & MASK_BG_COLOR) != (state & MASK_BG_COLOR)) { styles.add(bgColor().toAnsiBgArgs()); } - return Ansi.style(sb, styles.toArray()); + return Ansi.style(appendable, styles.toArray()); } } diff --git a/twinkle-chart/src/test/java/examples/BarDemo.java b/twinkle-chart/src/test/java/examples/BarDemo.java index a3b06b2..19f29ac 100644 --- a/twinkle-chart/src/test/java/examples/BarDemo.java +++ b/twinkle-chart/src/test/java/examples/BarDemo.java @@ -22,18 +22,18 @@ private static void printSimpleBar() { Panel pnl = Panel.of(20, 1); Bar b = Bar.bar().setValue(42); b.render(pnl); - System.out.println(pnl.toString()); + System.out.println(pnl); } private static void printHorizontalBars() { Panel pnl = Panel.of(20, 4); FracBarConfig cfg = FracBarConfig.create(); renderHorizontal(pnl, cfg); - System.out.println(pnl.toString()); + System.out.println(pnl); cfg.direction(BarConfig.Direction.R2L); renderHorizontal(pnl, cfg); - System.out.println(pnl.toString()); + System.out.println(pnl); } private static void renderHorizontal(Panel pnl, FracBarConfig cfg) { @@ -48,11 +48,11 @@ private static void printVerticalBars() { Panel pnl = Panel.of(16, 8); FracBarConfig cfg = FracBarConfig.create().direction(BarConfig.Direction.B2T); renderVertical(pnl, cfg); - System.out.println(pnl.toString()); + System.out.println(pnl); cfg.direction(BarConfig.Direction.T2B); renderVertical(pnl, cfg); - System.out.println(pnl.toString()); + System.out.println(pnl); } private static void renderVertical(Panel pnl, FracBarConfig cfg) { diff --git a/twinkle-chart/src/test/java/examples/MathPlotFourDemo.java b/twinkle-chart/src/test/java/examples/MathPlotFourDemo.java index 4ce9d0c..6ac6930 100644 --- a/twinkle-chart/src/test/java/examples/MathPlotFourDemo.java +++ b/twinkle-chart/src/test/java/examples/MathPlotFourDemo.java @@ -1,6 +1,8 @@ // java package examples; +import java.io.IOException; +import java.io.PrintWriter; import java.util.Random; import org.codejive.twinkle.ansi.Ansi; import org.codejive.twinkle.ansi.Color; @@ -15,7 +17,7 @@ import org.jspecify.annotations.NonNull; public class MathPlotFourDemo { - public static void main(String[] args) throws InterruptedException { + public static void main(String[] args) throws InterruptedException, IOException { Panel pnl = Panel.of(60, 40); AnimatingMathPlot p1 = new AnimatingMathPlot(Size.of(30, 20), " Interfering Waves 1 "); AnimatingMathPlot p2 = new AnimatingMathPlot(Size.of(30, 20), " Interfering Waves 2 "); @@ -26,11 +28,12 @@ public static void main(String[] args) throws InterruptedException { Canvas v3 = pnl.view(0, 20, 30, 20); Canvas v4 = pnl.view(30, 20, 30, 20); - System.out.print(Ansi.hideCursor()); + PrintWriter pout = new PrintWriter(System.out); + pout.print(Ansi.hideCursor()); try { for (int i = 0; i < 400; i++) { if (i > 0) { - System.out.print(Ansi.cursorMove(Ansi.CURSOR_PREV_LINE, pnl.size().height())); + pout.print(Ansi.cursorMove(Ansi.CURSOR_PREV_LINE, pnl.size().height())); } p1.update(); @@ -43,12 +46,13 @@ public static void main(String[] args) throws InterruptedException { p3.render(v3); p4.render(v4); - System.out.println(pnl.toAnsiString()); + pnl.toAnsi(pout); + pout.println(); Thread.sleep(20); } } finally { - System.out.print(Ansi.showCursor()); + pout.print(Ansi.showCursor()); } } } diff --git a/twinkle-core/src/main/java/org/codejive/twinkle/core/text/StyledBuffer.java b/twinkle-core/src/main/java/org/codejive/twinkle/core/text/StyledBuffer.java index 3b2cb83..6be21ad 100644 --- a/twinkle-core/src/main/java/org/codejive/twinkle/core/text/StyledBuffer.java +++ b/twinkle-core/src/main/java/org/codejive/twinkle/core/text/StyledBuffer.java @@ -1,9 +1,10 @@ package org.codejive.twinkle.core.text; +import org.codejive.twinkle.ansi.Printable; import org.codejive.twinkle.ansi.Style; import org.jspecify.annotations.NonNull; -public interface StyledBuffer extends StyledCharSequence { +public interface StyledBuffer extends StyledCharSequence, Printable { char REPLACEMENT_CHAR = '\uFFFD'; @@ -35,36 +36,6 @@ default int putStringAt(int index, @NonNull Style style, @NonNull CharSequence s @NonNull StyledBuffer resize(int newSize); - /** - * Converts the buffer to an ANSI string, including ANSI escape codes for styles. This method - * resets the current style to default at the start of the string. - * - * @return The ANSI string representation of the styled buffer. - */ - @NonNull String toAnsiString(); - - /** - * Converts the buffer to an ANSI string, including ANSI escape codes for styles. This method - * takes into account the provided current style to generate a result that is as efficient as - * possible in terms of ANSI codes. - * - * @param currentStyle The current style to start with. - * @return The ANSI string representation of the styled buffer. - */ - default @NonNull String toAnsiString(Style currentStyle) { - return toAnsiString(currentStyle.state()); - } - - /** - * Converts the buffer to an ANSI string, including ANSI escape codes for styles. This method - * takes into account the provided current style to generate a result that is as efficient as - * possible in terms of ANSI codes. - * - * @param currentStyle The current style to start with. - * @return The ANSI string representation of the styled buffer. - */ - @NonNull String toAnsiString(long currentStyle); - StyledBuffer EMPTY = new StyledCodepointBuffer(0) { @Override diff --git a/twinkle-core/src/main/java/org/codejive/twinkle/core/text/StyledCodepointBuffer.java b/twinkle-core/src/main/java/org/codejive/twinkle/core/text/StyledCodepointBuffer.java index 86d88b9..425a378 100644 --- a/twinkle-core/src/main/java/org/codejive/twinkle/core/text/StyledCodepointBuffer.java +++ b/twinkle-core/src/main/java/org/codejive/twinkle/core/text/StyledCodepointBuffer.java @@ -1,5 +1,6 @@ package org.codejive.twinkle.core.text; +import java.io.IOException; import org.codejive.twinkle.ansi.Ansi; import org.codejive.twinkle.ansi.Style; import org.jspecify.annotations.NonNull; @@ -255,8 +256,17 @@ private boolean outside(int index, int length) { // plus 20 extra for escape codes int initialCapacity = length() + 20; StringBuilder sb = new StringBuilder(initialCapacity); - sb.append(Ansi.STYLE_RESET); - return toAnsiString(sb, Style.UNSTYLED.state()).toString(); + try { + return toAnsi(sb).toString(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public @NonNull Appendable toAnsi(Appendable appendable) throws IOException { + appendable.append(Ansi.STYLE_RESET); + return toAnsi(appendable, Style.UNSTYLED.state()); } /** @@ -278,22 +288,36 @@ private boolean outside(int index, int length) { // plus 20 extra for escape codes int initialCapacity = length() + 20; StringBuilder sb = new StringBuilder(initialCapacity); - return toAnsiString(sb, styleState).toString(); + try { + return toAnsi(sb, styleState).toString(); + } catch (IOException e) { + throw new RuntimeException(e); + } } - private @NonNull StringBuilder toAnsiString(StringBuilder sb, long lastStyleState) { + @Override + public @NonNull Appendable toAnsi(Appendable appendable, long lastStyleState) + throws IOException { for (int i = 0; i < length(); i++) { if (styleBuffer[i] != lastStyleState) { Style style = Style.of(styleBuffer[i]); - style.toAnsiString(sb, lastStyleState); + style.toAnsi(appendable, lastStyleState); lastStyleState = styleBuffer[i]; } int cp = cpBuffer[i]; if (cp == '\0') { cp = ' '; } - sb.appendCodePoint(cp); + if (Character.isBmpCodePoint(cp)) { + appendable.append((char) cp); + } else if (Character.isValidCodePoint(cp)) { + appendable.append(Character.lowSurrogate(cp)); + appendable.append(Character.highSurrogate(cp)); + } else { + throw new IllegalArgumentException( + String.format("Not a valid Unicode code point: 0x%X", cp)); + } } - return sb; + return appendable; } } diff --git a/twinkle-core/src/main/java/org/codejive/twinkle/core/widget/Panel.java b/twinkle-core/src/main/java/org/codejive/twinkle/core/widget/Panel.java index 2cfeb4f..d8f4799 100644 --- a/twinkle-core/src/main/java/org/codejive/twinkle/core/widget/Panel.java +++ b/twinkle-core/src/main/java/org/codejive/twinkle/core/widget/Panel.java @@ -1,10 +1,10 @@ package org.codejive.twinkle.core.widget; -import org.codejive.twinkle.ansi.Style; +import org.codejive.twinkle.ansi.Printable; import org.codejive.twinkle.core.text.StyledBuffer; import org.jspecify.annotations.NonNull; -public interface Panel extends Canvas { +public interface Panel extends Canvas, Printable { @NonNull Panel resize(@NonNull Size newSize); @@ -16,14 +16,6 @@ public interface Panel extends Canvas { @Override @NonNull PanelView view(@NonNull Rect rect); - String toAnsiString(); - - default String toAnsiString(Style currentStyle) { - return toAnsiString(currentStyle.state()); - } - - String toAnsiString(long currentStyleState); - static @NonNull Panel of(int width, int height) { return of(Size.of(width, height)); } diff --git a/twinkle-core/src/main/java/org/codejive/twinkle/core/widget/StyledBufferPanel.java b/twinkle-core/src/main/java/org/codejive/twinkle/core/widget/StyledBufferPanel.java index bcd9c7e..6ee82f5 100644 --- a/twinkle-core/src/main/java/org/codejive/twinkle/core/widget/StyledBufferPanel.java +++ b/twinkle-core/src/main/java/org/codejive/twinkle/core/widget/StyledBufferPanel.java @@ -2,6 +2,7 @@ import static org.codejive.twinkle.core.text.StyledBuffer.REPLACEMENT_CHAR; +import java.io.IOException; import org.codejive.twinkle.ansi.Ansi; import org.codejive.twinkle.ansi.Style; import org.codejive.twinkle.core.text.StyledBuffer; @@ -206,33 +207,49 @@ public String toString() { } @Override - public String toAnsiString() { + public @NonNull String toAnsiString() { // Assuming only single-width characters for capacity estimation // plus 20 extra for escape codes and newline int initialCapacity = (size().width() + 20) * size().height(); StringBuilder sb = new StringBuilder(initialCapacity); sb.append(Ansi.STYLE_RESET); - return toAnsiString(sb, Style.F_UNSTYLED).toString(); + try { + return toAnsi(sb, Style.F_UNSTYLED).toString(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public @NonNull Appendable toAnsi(Appendable appendable) throws IOException { + appendable.append(Ansi.STYLE_RESET); + return toAnsi(appendable, Style.F_UNSTYLED); } @Override - public String toAnsiString(long currentStyleState) { + public @NonNull String toAnsiString(long currentStyleState) { // Assuming only single-width characters for capacity estimation // plus 20 extra for escape codes and newline int initialCapacity = (size().width() + 1) * size().height(); StringBuilder sb = new StringBuilder(initialCapacity); - return toAnsiString(sb, currentStyleState).toString(); + try { + return toAnsi(sb, currentStyleState).toString(); + } catch (IOException e) { + throw new RuntimeException(e); + } } - private @NonNull StringBuilder toAnsiString(StringBuilder sb, long currentStyleState) { + @Override + public @NonNull Appendable toAnsi(Appendable appendable, long currentStyleState) + throws IOException { for (int y = 0; y < size().height(); y++) { - sb.append(line(y).toAnsiString(currentStyleState)); + line(y).toAnsi(appendable, currentStyleState); currentStyleState = line(y).styleStateAt(size().width() - 1); if (y < size().height() - 1) { - sb.append('\n'); + appendable.append('\n'); } } - return sb; + return appendable; } public static class StyledBufferPanelView extends StyledBufferPanel implements PanelView {