From 38b69d66ccb200f9383726dda92a313a1d370ec5 Mon Sep 17 00:00:00 2001 From: Tako Schotanus Date: Wed, 7 Jan 2026 09:30:05 +0100 Subject: [PATCH 1/2] refactor: Styles can now deal with UNsetting attributes Before we could only set attributes, which then created confusion when we want to switch to another style. Are certain attributes unset because we don't care about them or do we actively want to unset them? This information is now kept in Style. --- .../java/org/codejive/twinkle/ansi/Style.java | 484 ++++++++++++------ .../codejive/twinkle/util/StyledIterator.java | 2 +- .../org/codejive/twinkle/ansi/TestStyle.java | 146 +++++- .../twinkle/util/TestStyledIterator.java | 9 +- .../codejive/twinkle/core/text/Buffer.java | 5 + .../twinkle/core/text/LineBuffer.java | 5 + .../org/codejive/twinkle/core/text/Span.java | 2 +- .../org/codejive/twinkle/widgets/Framed.java | 2 +- .../twinkle/core/text/TestBuffer.java | 15 +- .../codejive/twinkle/core/text/TestLine.java | 10 +- .../twinkle/core/text/TestLineBuffer.java | 8 +- .../codejive/twinkle/core/text/TestSpan.java | 2 +- .../codejive/twinkle/core/text/TestText.java | 9 +- 13 files changed, 492 insertions(+), 207 deletions(-) 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 3fed645..e93b99e 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 @@ -8,6 +8,7 @@ public class Style implements Printable { private final long state; + private final long mask; private static final long IDX_BOLD = 0; private static final long IDX_FAINT = 1; @@ -21,6 +22,7 @@ public class Style implements Printable { public static final long F_UNKNOWN = -1L; public static final long F_UNSTYLED = 0L; + public static final long F_BOLD = 1 << IDX_BOLD; public static final long F_FAINT = 1 << IDX_FAINT; public static final long F_ITALIC = 1 << IDX_ITALICIZED; @@ -30,17 +32,6 @@ public class Style implements Printable { public static final long F_HIDDEN = 1 << IDX_INVISIBLE; public static final long F_STRIKETHROUGH = 1 << IDX_CROSSEDOUT; - public static final Style UNKNOWN = new Style(F_UNKNOWN); - public static final Style UNSTYLED = new Style(F_UNSTYLED); - public static final Style BOLD = UNSTYLED.bold(); - public static final Style FAINT = UNSTYLED.faint(); - public static final Style ITALIC = UNSTYLED.italic(); - public static final Style UNDERLINED = UNSTYLED.underlined(); - public static final Style BLINK = UNSTYLED.blink(); - public static final Style INVERSE = UNSTYLED.inverse(); - public static final Style HIDDEN = UNSTYLED.hidden(); - public static final Style STRIKETHROUGH = UNSTYLED.strikethrough(); - public static @NonNull Style ofFgColor(@NonNull Color color) { return UNSTYLED.fgColor(color); } @@ -100,6 +91,28 @@ public class Style implements Printable { private static final long MASK_COLOR_BASIC_INTENSITY = 0x03L; private static final long MASK_COLOR_BASIC_INDEX = 0x07L; private static final long MASK_COLOR_PART = 0xffL; + private static final long MASK_STYLES = + F_BOLD + | F_FAINT + | F_ITALIC + | F_UNDERLINED + | F_BLINK + | F_INVERSE + | F_HIDDEN + | F_STRIKETHROUGH; + private static final long MASK_ALL = MASK_FG_COLOR | MASK_BG_COLOR | MASK_STYLES; + + public static final Style UNKNOWN = new Style(F_UNKNOWN, 0); + public static final Style UNSTYLED = new Style(F_UNSTYLED, 0); + public static final Style DEFAULT = new Style(F_UNSTYLED, MASK_ALL); + public static final Style BOLD = UNSTYLED.bold(); + public static final Style FAINT = UNSTYLED.faint(); + public static final Style ITALIC = UNSTYLED.italic(); + public static final Style UNDERLINED = UNSTYLED.underlined(); + public static final Style BLINK = UNSTYLED.blink(); + public static final Style INVERSE = UNSTYLED.inverse(); + public static final Style HIDDEN = UNSTYLED.hidden(); + public static final Style STRIKETHROUGH = UNSTYLED.strikethrough(); private static final long CM_INDEXED = 0; private static final long CM_RGB = 1; @@ -109,141 +122,287 @@ public class Style implements Printable { // Not really an intensity, but a flag to indicate default color, // but we're (ab)using the intensity bits to store it - private static final long INTENSITY_DEFAULT = 0; + private static final int INTENSITY_DEFAULT = 0; - private static final long INTENSITY_NORMAL = 1; - private static final long INTENSITY_DARK = 2; - private static final long INTENSITY_BRIGHT = 3; + private static final int INTENSITY_NORMAL = 1; + private static final int INTENSITY_DARK = 2; + private static final int INTENSITY_BRIGHT = 3; public static @NonNull Style of(long state) { - if (state == 0) { - return UNSTYLED; + if (state == F_UNKNOWN) { + return UNKNOWN; + } + if (state == F_UNSTYLED) { + return DEFAULT; } - return new Style(state); + return new Style(state, MASK_ALL); } - private Style(long state) { + public static @NonNull Style of(long state, long mask) { + if (state == F_UNKNOWN) { + return UNKNOWN; + } + if (state == F_UNSTYLED) { + if ((mask & MASK_ALL) == 0) { + return UNSTYLED; + } else if ((mask & MASK_ALL) == MASK_ALL) { + return DEFAULT; + } + } + return new Style(state, mask); + } + + private Style(long state, long mask) { this.state = state; + this.mask = mask; } public long state() { return state; } - public @NonNull Style unstyled() { - return UNSTYLED; + public long mask() { + return mask; } - public @NonNull Style normal() { - return of(state & ~(F_BOLD | F_FAINT)); + /** + * Combines this style with another style, giving precedence to the other style's values + * wherever it has an effect. + * + * @param other The other style to combine with. + * @return A new Style instance representing the combined style. + */ + public Style and(@NonNull Style other) { + if (this.equals(UNKNOWN)) { + return other; + } + if (other.equals(UNKNOWN)) { + return this; + } + + long newState = (this.state & ~other.mask) | (other.state & other.mask); + long newMask = this.mask | other.mask; + return of(newState, newMask); } - public boolean isBold() { - return (state & F_BOLD) != 0; + /** + * Computes the difference between this style and another style, producing a new style that + * represents the changes needed to transform this style into the other style. + * + * @param other The other style to compare with. + * @return A new Style instance representing the difference. + */ + public Style diff(@NonNull Style other) { + if (this.equals(UNKNOWN)) { + return other; + } + if (other.equals(UNKNOWN)) { + return this; + } + + long newMask = this.mask | other.mask; + long newState = other.state & newMask; + return of(newState, newMask); } - public @NonNull Style bold() { - return of(state | F_BOLD); + /** + * Returns a new style that represents the style that would result from applying the other style + * on top of this one. Styles that are changed to their unset or default values in the resulting + * style will be marked as unaffected. + * + * @param other The other style to apply. + * @return A new Style instance representing the resulting style. + */ + public Style apply(@NonNull Style other) { + if (this.equals(UNKNOWN)) { + return other; + } + if (other.equals(UNKNOWN)) { + return this; + } + + long newState = (this.state & ~other.mask) | (other.state & other.mask); + long newMask = this.mask | other.mask; + + // now mark unset styles as unaffected + long unaffectedMask = ~(other.mask & ~other.state & MASK_STYLES); + newMask &= unaffectedMask; + + if (other.affectsFgColor() && other.fgColor().equals(Color.DEFAULT)) { + newState &= ~MASK_FG_COLOR; + newMask &= ~MASK_FG_COLOR; + } + + if (other.affectsBgColor() && other.bgColor().equals(Color.DEFAULT)) { + newState &= ~MASK_BG_COLOR; + newMask &= ~MASK_BG_COLOR; + } + + return of(newState, newMask); + } + + public boolean is(long flag) { + return (state & flag) != 0; + } + + public boolean isBold() { + return is(F_BOLD); } public boolean isFaint() { - return (state & F_FAINT) != 0; + return is(F_FAINT); + } + + public boolean isItalic() { + return is(F_ITALIC); + } + + public boolean isUnderlined() { + return is(F_UNDERLINED); + } + + public boolean isBlink() { + return is(F_BLINK); + } + + public boolean isInverse() { + return is(F_INVERSE); + } + + public boolean isHidden() { + return is(F_HIDDEN); + } + + public boolean isStrikethrough() { + return is(F_STRIKETHROUGH); + } + + public @NonNull Color fgColor() { + long fgc = ((state & MASK_FG_COLOR) >> SHIFT_FG_COLOR); + return decodeColor(fgc); + } + + public @NonNull Color bgColor() { + long bgc = ((state & MASK_BG_COLOR) >> SHIFT_BG_COLOR); + return decodeColor(bgc); + } + + public boolean affects(long flag) { + return (mask & flag) != 0; + } + + public boolean affectsBold() { + return affects(F_BOLD); + } + + public boolean affectsFaint() { + return affects(F_FAINT); + } + + public boolean affectsItalic() { + return affects(F_ITALIC); + } + + public boolean affectsUnderlined() { + return affects(F_UNDERLINED); + } + + public boolean affectsBlink() { + return affects(F_BLINK); + } + + public boolean affectsInverse() { + return affects(F_INVERSE); + } + + public boolean affectsHidden() { + return affects(F_HIDDEN); + } + + public boolean affectsStrikethrough() { + return affects(F_STRIKETHROUGH); + } + + public boolean affectsFgColor() { + return affects(MASK_FG_COLOR); + } + + public boolean affectsBgColor() { + return affects(MASK_BG_COLOR); + } + + public @NonNull Style reset() { + return DEFAULT; + } + + public @NonNull Style bold() { + return of(state | F_BOLD, mask | F_BOLD); } public @NonNull Style faint() { - return of(state | F_FAINT); + return of(state | F_FAINT, mask | F_FAINT); } - public boolean isItalic() { - return (state & F_ITALIC) != 0; + public @NonNull Style normal() { + return of(state & ~(F_BOLD | F_FAINT), mask | F_BOLD | F_FAINT); } public @NonNull Style italic() { - return of(state | F_ITALIC); + return of(state | F_ITALIC, mask | F_ITALIC); } public @NonNull Style italicOff() { - return of(state & ~F_ITALIC); - } - - public boolean isUnderlined() { - return (state & F_UNDERLINED) != 0; + return of(state & ~F_ITALIC, mask | F_ITALIC); } public @NonNull Style underlined() { - return of(state | F_UNDERLINED); + return of(state | F_UNDERLINED, mask | F_UNDERLINED); } public @NonNull Style underlinedOff() { - return of(state & ~F_UNDERLINED); - } - - public boolean isBlink() { - return (state & F_BLINK) != 0; + return of(state & ~F_UNDERLINED, mask | F_UNDERLINED); } public @NonNull Style blink() { - return of(state | F_BLINK); + return of(state | F_BLINK, mask | F_BLINK); } public @NonNull Style blinkOff() { - return of(state & ~F_BLINK); - } - - public boolean isInverse() { - return (state & F_INVERSE) != 0; + return of(state & ~F_BLINK, mask | F_BLINK); } public @NonNull Style inverse() { - return of(state | F_INVERSE); + return of(state | F_INVERSE, mask | F_INVERSE); } public @NonNull Style inverseOff() { - return of(state & ~F_INVERSE); - } - - public boolean isHidden() { - return (state & F_HIDDEN) != 0; + return of(state & ~F_INVERSE, mask | F_INVERSE); } public @NonNull Style hidden() { - return of(state | F_HIDDEN); + return of(state | F_HIDDEN, mask | F_HIDDEN); } public @NonNull Style hiddenOff() { - return of(state & ~F_HIDDEN); - } - - public boolean isStrikethrough() { - return (state & F_STRIKETHROUGH) != 0; + return of(state & ~F_HIDDEN, mask | F_HIDDEN); } public @NonNull Style strikethrough() { - return of(state | F_STRIKETHROUGH); + return of(state | F_STRIKETHROUGH, mask | F_STRIKETHROUGH); } public @NonNull Style strikethroughOff() { - return of(state & ~F_STRIKETHROUGH); - } - - public @NonNull Color fgColor() { - long fgc = ((state & MASK_FG_COLOR) >> SHIFT_FG_COLOR); - return decodeColor(fgc); + return of(state & ~F_STRIKETHROUGH, mask | F_STRIKETHROUGH); } public @NonNull Style fgColor(@NonNull Color color) { long newState = applyFgColor(state, color); - return of(newState); - } - - public @NonNull Color bgColor() { - long bgc = ((state & MASK_BG_COLOR) >> SHIFT_BG_COLOR); - return decodeColor(bgc); + return of(newState, mask | MASK_FG_COLOR); } public @NonNull Style bgColor(@NonNull Color color) { long newState = applyBgColor(state, color); - return of(newState); + return of(newState, mask | MASK_BG_COLOR); } private static long encodeColor(@NonNull Color color) { @@ -285,7 +444,7 @@ private static long encodeColor(@NonNull Color color) { } private static @NonNull Color decodeColor(long color) { - Color result = Color.DEFAULT; + Color result; long mode = color & MASK_COLOR_MODE; if (mode == CM_INDEXED) { long paletteType = (color >> SHIFT_PALETTE_TYPE) & MASK_PALETTE_TYPE; @@ -295,15 +454,19 @@ private static long encodeColor(@NonNull Color color) { int colorIndex = (int) ((color >> SHIFT_COLOR_BASIC_INDEX) & MASK_COLOR_BASIC_INDEX); switch (intensity) { - case 1: + case INTENSITY_NORMAL: result = Color.basic(colorIndex, Color.BasicColor.Intensity.normal); break; - case 2: + case INTENSITY_DARK: result = Color.basic(colorIndex, Color.BasicColor.Intensity.dark); break; - case 3: + case INTENSITY_BRIGHT: result = Color.basic(colorIndex, Color.BasicColor.Intensity.bright); break; + case INTENSITY_DEFAULT: + default: + result = Color.DEFAULT; + break; } } else { // paletteType == F_PALETTE_INDEXED int colorIndex = (int) ((color >> SHIFT_COLOR_INDEXED_INDEX) & MASK_COLOR_PART); @@ -318,13 +481,9 @@ private static long encodeColor(@NonNull Color color) { return result; } - public static long parse(@NonNull String ansiSequence) { - return parse(F_UNSTYLED, ansiSequence); - } - - public static long parse(long currentStyleState, @NonNull String ansiSequence) { + public static Style parse(@NonNull String ansiSequence) { if (!ansiSequence.startsWith(Ansi.CSI) || !ansiSequence.endsWith("m")) { - return currentStyleState; + return UNSTYLED; } String content = ansiSequence.substring(2, ansiSequence.length() - 1); @@ -339,7 +498,7 @@ public static long parse(long currentStyleState, @NonNull String ansiSequence) { } } - long state = currentStyleState; + Style style = UNSTYLED; for (int i = 0; i < codes.length; i++) { int code = codes[i]; switch (code) { @@ -347,72 +506,72 @@ public static long parse(long currentStyleState, @NonNull String ansiSequence) { // Invalid code, ignore break; case 0: - state = 0; + style = style.reset(); break; case 1: - state |= F_BOLD; + style = style.bold(); break; case 2: - state |= F_FAINT; + style = style.faint(); break; case 3: - state |= F_ITALIC; + style = style.italic(); break; case 4: - state |= F_UNDERLINED; + style = style.underlined(); break; case 5: - state |= F_BLINK; + style = style.blink(); break; case 7: - state |= F_INVERSE; + style = style.inverse(); break; case 8: - state |= F_HIDDEN; + style = style.hidden(); break; case 9: - state |= F_STRIKETHROUGH; + style = style.strikethrough(); break; case 22: - state &= ~(F_BOLD | F_FAINT); + style = style.normal(); break; case 23: - state &= ~F_ITALIC; + style = style.italicOff(); break; case 24: - state &= ~F_UNDERLINED; + style = style.underlinedOff(); break; case 25: - state &= ~F_BLINK; + style = style.blinkOff(); break; case 27: - state &= ~F_INVERSE; + style = style.inverseOff(); break; case 28: - state &= ~F_HIDDEN; + style = style.hiddenOff(); break; case 29: - state &= ~F_STRIKETHROUGH; + style = style.strikethroughOff(); break; case 39: - state &= ~MASK_FG_COLOR; + style = style.fgColor(Color.DEFAULT); break; case 49: - state &= ~MASK_BG_COLOR; + style = style.bgColor(Color.DEFAULT); break; default: if (code >= 30 && code <= 37) { Color c = Color.basic(code - 30, Color.BasicColor.Intensity.normal); - state = applyFgColor(state, c); + style = style.fgColor(c); } else if (code >= 90 && code <= 97) { Color c = Color.basic(code - 90, Color.BasicColor.Intensity.bright); - state = applyFgColor(state, c); + style = style.fgColor(c); } else if (code >= 40 && code <= 47) { Color c = Color.basic(code - 40, Color.BasicColor.Intensity.normal); - state = applyBgColor(state, c); + style = style.bgColor(c); } else if (code >= 100 && code <= 107) { Color c = Color.basic(code - 100, Color.BasicColor.Intensity.bright); - state = applyBgColor(state, c); + style = style.bgColor(c); } else if (code == 38 || code == 48) { boolean isFg = (code == 38); if (i + 1 < codes.length) { @@ -420,17 +579,17 @@ public static long parse(long currentStyleState, @NonNull String ansiSequence) { if (type == 5 && i + 2 < codes.length) { Color c = Color.indexed(codes[i + 2]); if (isFg) { - state = applyFgColor(state, c); + style = style.fgColor(c); } else { - state = applyBgColor(state, c); + style = style.bgColor(c); } i += 2; } else if (type == 2 && i + 4 < codes.length) { Color c = Color.rgb(codes[i + 2], codes[i + 3], codes[i + 4]); if (isFg) { - state = applyFgColor(state, c); + style = style.fgColor(c); } else { - state = applyBgColor(state, c); + style = style.bgColor(c); } i += 4; } @@ -439,7 +598,7 @@ public static long parse(long currentStyleState, @NonNull String ansiSequence) { break; } } - return state; + return style; } private static long applyFgColor(long state, Color color) { @@ -457,12 +616,12 @@ public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Style)) return false; Style other = (Style) o; - return this.state == other.state; + return this.state == other.state && this.mask == other.mask; } @Override public int hashCode() { - return Long.hashCode(state); + return Long.hashCode(state) * 31 + Long.hashCode(mask); } @Override @@ -473,114 +632,107 @@ public String toString() { sb.append("UNKNOWN}"); return sb.toString(); } - if (isBold()) sb.append("bold, "); - if (isFaint()) sb.append("faint, "); - if (isItalic()) sb.append("italic, "); - if (isUnderlined()) sb.append("underlined, "); - if (isBlink()) sb.append("blink, "); - if (isInverse()) sb.append("inverse, "); - if (isHidden()) sb.append("hidden, "); - if (isStrikethrough()) sb.append("strikethrough, "); - if (fgColor() != Color.DEFAULT) sb.append("fgColor=").append(fgColor()).append(", "); - if (bgColor() != Color.DEFAULT) sb.append("bgColor=").append(bgColor()); + if (this.equals(DEFAULT)) { + sb.append("DEFAULT}"); + return sb.toString(); + } + if (affectsBold() && affectsFaint() && !isBold() && !isFaint()) { + sb.append("normal, "); + } else { + if (affectsBold()) sb.append(isBold() ? "bold, " : "-bold, "); + if (affectsFaint()) sb.append(isFaint() ? "faint, " : "-faint, "); + } + if (affectsItalic()) sb.append(isItalic() ? "italic, " : "-italic, "); + if (affectsUnderlined()) sb.append(isUnderlined() ? "underlined, " : "-underlined, "); + if (affectsBlink()) sb.append(isBlink() ? "blink, " : "-blink, "); + if (affectsInverse()) sb.append(isInverse() ? "inverse, " : "-inverse, "); + if (affectsHidden()) sb.append(isHidden() ? "hidden, " : "-hidden, "); + if (affectsStrikethrough()) + sb.append(isStrikethrough() ? "strikethrough, " : "-strikethrough, "); + if (affectsFgColor()) sb.append("fgColor=").append(fgColor()).append(", "); + if (affectsBgColor()) sb.append("bgColor=").append(bgColor()); if (sb.charAt(sb.length() - 2) == ',') sb.setLength(sb.length() - 2); // Remove trailing comma sb.append('}'); return sb.toString(); } - @Override - public @NonNull String toAnsiString() { - return toAnsiString(UNSTYLED); - } - - @Override - public @NonNull Appendable toAnsi(Appendable appendable) throws IOException { - try { - return toAnsi(appendable, Style.UNSTYLED); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - @Override - public @NonNull String toAnsiString(Style currentStyle) { - try { - return toAnsi(new StringBuilder(), currentStyle).toString(); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - @Override public @NonNull Appendable toAnsi(Appendable appendable, Style currentStyle) throws IOException { if (this.equals(UNKNOWN)) { // Do nothing, we keep the current state return appendable; } - if (currentStyle.equals(UNKNOWN)) { - appendable.append(Ansi.STYLE_RESET); - currentStyle = UNSTYLED; - } List styles = new ArrayList<>(); - if ((currentStyle.state() & (F_BOLD | F_FAINT)) != (state & (F_BOLD | F_FAINT))) { - // First we switch to NORMAL to clear both BOLD and FAINT - if (currentStyle.isBold() || currentStyle.isFaint()) { + if (shouldApply(currentStyle, F_BOLD) || shouldApply(currentStyle, F_FAINT)) { + boolean normal = false; + if (!currentStyle.equals(UNKNOWN) + && ((!isBold() && currentStyle.isBold()) + || (!isFaint() && currentStyle.isFaint()))) { + // First we switch to NORMAL to clear both BOLD and FAINT styles.add(Ansi.NORMAL); + normal = true; } // Now we set the needed styles - if (isBold()) styles.add(Ansi.BOLD); - if (isFaint()) styles.add(Ansi.FAINT); + if (isBold() && (normal || !currentStyle.affectsBold() || !currentStyle.isBold())) + styles.add(Ansi.BOLD); + if (isFaint() && (normal || !currentStyle.affectsFaint() || !currentStyle.isFaint())) + styles.add(Ansi.FAINT); } - if (currentStyle.isItalic() != isItalic()) { + if (shouldApply(currentStyle, F_ITALIC)) { if (isItalic()) { styles.add(Ansi.ITALICIZED); } else { styles.add(Ansi.NOTITALICIZED); } } - if (currentStyle.isUnderlined() != isUnderlined()) { + if (shouldApply(currentStyle, F_UNDERLINED)) { if (isUnderlined()) { styles.add(Ansi.UNDERLINED); } else { styles.add(Ansi.NOTUNDERLINED); } } - if (currentStyle.isBlink() != isBlink()) { + if (shouldApply(currentStyle, F_BLINK)) { if (isBlink()) { styles.add(Ansi.BLINK); } else { styles.add(Ansi.STEADY); } } - if (currentStyle.isInverse() != isInverse()) { + if (shouldApply(currentStyle, F_INVERSE)) { if (isInverse()) { styles.add(Ansi.INVERSE); } else { styles.add(Ansi.POSITIVE); } } - if (currentStyle.isHidden() != isHidden()) { + if (shouldApply(currentStyle, F_HIDDEN)) { if (isHidden()) { styles.add(Ansi.INVISIBLE); } else { styles.add(Ansi.VISIBLE); } } - if (currentStyle.isStrikethrough() != isStrikethrough()) { + if (shouldApply(currentStyle, F_STRIKETHROUGH)) { if (isStrikethrough()) { styles.add(Ansi.CROSSEDOUT); } else { styles.add(Ansi.NOTCROSSEDOUT); } } - if ((currentStyle.state() & MASK_FG_COLOR) != (state & MASK_FG_COLOR)) { + if (affectsFgColor() + && (!currentStyle.affectsFgColor() || !fgColor().equals(currentStyle.fgColor()))) { styles.add(fgColor().toAnsiFgArgs()); } - if ((currentStyle.state() & MASK_BG_COLOR) != (state & MASK_BG_COLOR)) { + if (affectsBgColor() + && (!currentStyle.affectsBgColor() || !bgColor().equals(currentStyle.bgColor()))) { styles.add(bgColor().toAnsiBgArgs()); } return Ansi.style(appendable, styles.toArray()); } + + private boolean shouldApply(Style otherStyle, long flag) { + return affects(flag) && (!otherStyle.affects(flag) || is(flag) != otherStyle.is(flag)); + } } diff --git a/twinkle-ansi/src/main/java/org/codejive/twinkle/util/StyledIterator.java b/twinkle-ansi/src/main/java/org/codejive/twinkle/util/StyledIterator.java index ad3368c..fc8aeec 100644 --- a/twinkle-ansi/src/main/java/org/codejive/twinkle/util/StyledIterator.java +++ b/twinkle-ansi/src/main/java/org/codejive/twinkle/util/StyledIterator.java @@ -95,7 +95,7 @@ private void primeNext() { if (cp == Ansi.ESC) { String ansiSequence = delegate.sequence(); if (ansiSequence.startsWith(Ansi.CSI) && ansiSequence.endsWith("m")) { - currentStyle = Style.of(Style.parse(currentStyle.state(), ansiSequence)); + currentStyle = currentStyle.apply(Style.parse(ansiSequence)); } } else { nextCodePoint = cp; diff --git a/twinkle-ansi/src/test/java/org/codejive/twinkle/ansi/TestStyle.java b/twinkle-ansi/src/test/java/org/codejive/twinkle/ansi/TestStyle.java index 84e0029..d95e3f9 100644 --- a/twinkle-ansi/src/test/java/org/codejive/twinkle/ansi/TestStyle.java +++ b/twinkle-ansi/src/test/java/org/codejive/twinkle/ansi/TestStyle.java @@ -7,7 +7,7 @@ public class TestStyle { @Test public void testStyleCreation() { - Style style1 = Style.of(Style.F_BOLD); + Style style1 = Style.UNSTYLED.bold(); Style style2 = Style.BOLD; assertThat(style1).isEqualTo(style2); @@ -30,6 +30,14 @@ public void testStyleCombination() { .inverse() .hidden() .strikethrough(); + assertThat(style.affectsBold()).isTrue(); + assertThat(style.affectsFaint()).isTrue(); + assertThat(style.affectsItalic()).isTrue(); + assertThat(style.affectsUnderlined()).isTrue(); + assertThat(style.affectsBlink()).isTrue(); + assertThat(style.affectsInverse()).isTrue(); + assertThat(style.affectsHidden()).isTrue(); + assertThat(style.affectsStrikethrough()).isTrue(); assertThat(style.isBold()).isTrue(); assertThat(style.isFaint()).isTrue(); assertThat(style.isItalic()).isTrue(); @@ -47,7 +55,56 @@ public void testStyleCombination() { .inverseOff() .hiddenOff() .strikethroughOff(); - assertThat(style).isEqualTo(Style.UNSTYLED); + assertThat(style.affectsBold()).isTrue(); + assertThat(style.affectsFaint()).isTrue(); + assertThat(style.affectsItalic()).isTrue(); + assertThat(style.affectsUnderlined()).isTrue(); + assertThat(style.affectsBlink()).isTrue(); + assertThat(style.affectsInverse()).isTrue(); + assertThat(style.affectsHidden()).isTrue(); + assertThat(style.affectsStrikethrough()).isTrue(); + assertThat(style.isBold()).isFalse(); + assertThat(style.isFaint()).isFalse(); + assertThat(style.isItalic()).isFalse(); + assertThat(style.isUnderlined()).isFalse(); + assertThat(style.isBlink()).isFalse(); + assertThat(style.isInverse()).isFalse(); + assertThat(style.isHidden()).isFalse(); + assertThat(style.isStrikethrough()).isFalse(); + } + + @Test + public void testUnsetStyle() { + Style style = Style.UNSTYLED.underlinedOff(); + assertThat(style.affectsUnderlined()).isTrue(); + assertThat(style.isUnderlined()).isFalse(); + assertThat(style.toAnsiString()).isEqualTo(Ansi.style(Ansi.NOTUNDERLINED)); + } + + @Test + public void testUnsetStyleAnd() { + Style style1 = Style.UNSTYLED.blink().underlined(); + Style style2 = Style.UNSTYLED.underlinedOff(); + + Style style3 = style1.and(style2); + + assertThat(style3.affectsBlink()).isTrue(); + assertThat(style3.isBlink()).isTrue(); + assertThat(style3.affectsUnderlined()).isTrue(); + assertThat(style3.isUnderlined()).isFalse(); + } + + @Test + public void testUnsetStyleApply() { + Style style1 = Style.UNSTYLED.blink().underlined(); + Style style2 = Style.UNSTYLED.underlinedOff(); + + Style style3 = style1.apply(style2); + + assertThat(style3.affectsBlink()).isTrue(); + assertThat(style3.isBlink()).isTrue(); + assertThat(style3.affectsUnderlined()).isFalse(); + assertThat(style3.isUnderlined()).isFalse(); } @Test @@ -91,9 +148,9 @@ public void testMixedStyles() { .blink() .inverse() .hidden() - .strikethrough(); - style = style.fgColor(Color.BasicColor.BLUE); - style = style.bgColor(Color.indexed(128)); + .strikethrough() + .fgColor(Color.BasicColor.BLUE) + .bgColor(Color.indexed(128)); style = style.normal() .italicOff() @@ -101,9 +158,38 @@ public void testMixedStyles() { .blinkOff() .inverseOff() .hiddenOff() - .strikethroughOff(); - style = style.fgColor(Color.DEFAULT); - style = style.bgColor(Color.DEFAULT); + .strikethroughOff() + .fgColor(Color.DEFAULT) + .bgColor(Color.DEFAULT); + assertThat(style).isEqualTo(Style.DEFAULT); + } + + @Test + public void testMixedStylesApply() { + Style style = + Style.UNSTYLED + .bold() + .faint() + .italic() + .underlined() + .blink() + .inverse() + .hidden() + .strikethrough() + .fgColor(Color.BasicColor.BLUE) + .bgColor(Color.indexed(128)); + style = + style.apply( + Style.UNSTYLED + .normal() + .italicOff() + .underlinedOff() + .blinkOff() + .inverseOff() + .hiddenOff() + .strikethroughOff() + .fgColor(Color.DEFAULT) + .bgColor(Color.DEFAULT)); assertThat(style).isEqualTo(Style.UNSTYLED); } @@ -140,6 +226,32 @@ public void testToAnsiStringAllStyles() { Ansi.CROSSEDOUT)); } + @Test + public void testToAnsiStringAllStylesWithDefault() { + Style style = + Style.UNSTYLED + .bold() + .faint() + .italic() + .underlined() + .blink() + .inverse() + .hidden() + .strikethrough(); + String ansiCode = style.toAnsiString(Style.DEFAULT); + assertThat(ansiCode) + .isEqualTo( + Ansi.style( + Ansi.BOLD, + Ansi.FAINT, + Ansi.ITALICIZED, + Ansi.UNDERLINED, + Ansi.BLINK, + Ansi.INVERSE, + Ansi.INVISIBLE, + Ansi.CROSSEDOUT)); + } + @Test public void testToAnsiStringAllStylesWithCurrent() { Style style = @@ -157,8 +269,6 @@ public void testToAnsiStringAllStylesWithCurrent() { assertThat(ansiCode) .isEqualTo( Ansi.style( - Ansi.NORMAL, - Ansi.BOLD, Ansi.FAINT, Ansi.ITALICIZED, Ansi.BLINK, @@ -190,4 +300,20 @@ public void testToAnsiStringAllStylesWithCurrent2() { Ansi.INVISIBLE, Ansi.CROSSEDOUT)); } + + @Test + public void testToAnsiStringNormal() { + Style style = Style.UNSTYLED.faint(); + Style currentStyle = Style.UNSTYLED.bold(); + String ansiCode = style.toAnsiString(currentStyle); + assertThat(ansiCode).isEqualTo(Ansi.style(Ansi.NORMAL, Ansi.FAINT)); + } + + @Test + public void testToAnsiStringNoNormal() { + Style style = Style.UNSTYLED.bold().faint(); + Style currentStyle = Style.UNSTYLED.bold(); + String ansiCode = style.toAnsiString(currentStyle); + assertThat(ansiCode).isEqualTo(Ansi.style(Ansi.FAINT)); + } } diff --git a/twinkle-ansi/src/test/java/org/codejive/twinkle/util/TestStyledIterator.java b/twinkle-ansi/src/test/java/org/codejive/twinkle/util/TestStyledIterator.java index a4a19df..ea0b274 100644 --- a/twinkle-ansi/src/test/java/org/codejive/twinkle/util/TestStyledIterator.java +++ b/twinkle-ansi/src/test/java/org/codejive/twinkle/util/TestStyledIterator.java @@ -2,6 +2,7 @@ import static org.assertj.core.api.Assertions.assertThat; +import org.codejive.twinkle.ansi.Ansi; import org.codejive.twinkle.ansi.Style; import org.junit.jupiter.api.Test; @@ -30,7 +31,7 @@ public void testPlainSequence() { @Test public void testStyledSequence() { - String red = "\u001B[31m"; + String red = Ansi.CSI + "31m"; SequenceIterator seqIter = SequenceIterator.of(red + "a"); StyledIterator it = new StyledIterator(seqIter); @@ -47,7 +48,7 @@ public void testStyledSequence() { @Test public void testSkipNonStyleAnsi() { - String up = "\u001B[1A"; // Cursor Up + String up = Ansi.cursorMove(Ansi.CURSOR_UP); SequenceIterator seqIter = SequenceIterator.of(up + "a"); StyledIterator it = new StyledIterator(seqIter); @@ -61,8 +62,8 @@ public void testSkipNonStyleAnsi() { @Test public void testMixedSequences() { - String red = "\u001B[31m"; - String reset = "\u001B[0m"; + String red = Ansi.CSI + "31m"; + String reset = Ansi.STYLE_RESET; SequenceIterator seqIter = SequenceIterator.of("a" + red + "b" + reset + "c"); StyledIterator it = new StyledIterator(seqIter); diff --git a/twinkle-core/src/main/java/org/codejive/twinkle/core/text/Buffer.java b/twinkle-core/src/main/java/org/codejive/twinkle/core/text/Buffer.java index 461e10c..90b644e 100644 --- a/twinkle-core/src/main/java/org/codejive/twinkle/core/text/Buffer.java +++ b/twinkle-core/src/main/java/org/codejive/twinkle/core/text/Buffer.java @@ -3,6 +3,7 @@ import static org.codejive.twinkle.core.text.LineBuffer.REPLACEMENT_CHAR; import java.io.IOException; +import org.codejive.twinkle.ansi.Ansi; import org.codejive.twinkle.ansi.Style; import org.codejive.twinkle.core.util.Rect; import org.codejive.twinkle.core.util.Size; @@ -245,6 +246,10 @@ public String toString() { @Override public @NonNull Appendable toAnsi(Appendable appendable, Style currentStyle) throws IOException { + if (currentStyle == Style.UNKNOWN) { + currentStyle = Style.DEFAULT; + appendable.append(Ansi.STYLE_RESET); + } for (int y = 0; y < size().height(); y++) { line(y).toAnsi(appendable, currentStyle); currentStyle = line(y).styleAt(size().width() - 1); diff --git a/twinkle-core/src/main/java/org/codejive/twinkle/core/text/LineBuffer.java b/twinkle-core/src/main/java/org/codejive/twinkle/core/text/LineBuffer.java index 5e0fe42..2325efd 100644 --- a/twinkle-core/src/main/java/org/codejive/twinkle/core/text/LineBuffer.java +++ b/twinkle-core/src/main/java/org/codejive/twinkle/core/text/LineBuffer.java @@ -1,6 +1,7 @@ package org.codejive.twinkle.core.text; import java.io.IOException; +import org.codejive.twinkle.ansi.Ansi; import org.codejive.twinkle.ansi.Style; import org.codejive.twinkle.util.Printable; import org.codejive.twinkle.util.StyledIterator; @@ -325,6 +326,10 @@ private boolean outside(int index, int length) { @Override public @NonNull Appendable toAnsi(Appendable appendable, Style currentStyle) throws IOException { + if (currentStyle == Style.UNKNOWN) { + currentStyle = Style.DEFAULT; + appendable.append(Ansi.STYLE_RESET); + } for (int i = 0; i < length(); i++) { if (styleBuffer[i] != currentStyle.state()) { Style style = Style.of(styleBuffer[i]); diff --git a/twinkle-core/src/main/java/org/codejive/twinkle/core/text/Span.java b/twinkle-core/src/main/java/org/codejive/twinkle/core/text/Span.java index acc2570..4f9d7be 100644 --- a/twinkle-core/src/main/java/org/codejive/twinkle/core/text/Span.java +++ b/twinkle-core/src/main/java/org/codejive/twinkle/core/text/Span.java @@ -44,7 +44,7 @@ public String toString() { @Override public @NonNull Appendable toAnsi(Appendable appendable, Style currentStyle) throws IOException { - style.toAnsi(appendable, currentStyle); + currentStyle.diff(style).toAnsi(appendable, currentStyle); appendable.append(text); return appendable; } diff --git a/twinkle-core/src/main/java/org/codejive/twinkle/widgets/Framed.java b/twinkle-core/src/main/java/org/codejive/twinkle/widgets/Framed.java index e6067bc..f5bef23 100644 --- a/twinkle-core/src/main/java/org/codejive/twinkle/widgets/Framed.java +++ b/twinkle-core/src/main/java/org/codejive/twinkle/widgets/Framed.java @@ -58,7 +58,7 @@ public void render(Canvas canvas) { } if (title != null) { Canvas view = canvas.view(2, 0, canvas.size().width() - 4, 1); - view.putStringAt(0, 0, Style.UNKNOWN, title.toAnsiString()); + view.putStringAt(0, 0, Style.DEFAULT, title.toAnsiString()); } if (widget != null) { widget.render(canvas.view(Rect.of(canvas.size()).grow(-1, -1, -1, -1))); diff --git a/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestBuffer.java b/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestBuffer.java index f3465b7..c02b671 100644 --- a/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestBuffer.java +++ b/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestBuffer.java @@ -24,7 +24,7 @@ public void testPanelDefaultInnerContent() { for (int y = 0; y < size.height(); y++) { for (int x = 0; x < size.width(); x++) { assertThat(buffer.charAt(x, y)).isEqualTo('\0'); - assertThat(buffer.styleAt(x, y)).isEqualTo(Style.UNSTYLED); + assertThat(buffer.styleAt(x, y)).isEqualTo(Style.DEFAULT); } } } @@ -51,7 +51,7 @@ public void testPanelNewContents() { for (int y = 0; y < size.height(); y++) { for (int x = 0; x < size.width(); x++) { assertThat(buffer.charAt(x, y)).isEqualTo((char) ('A' + x + y * size.width())); - assertThat(buffer.styleAt(x, y)).isEqualTo(Style.ofFgColor(Color.indexed(x))); + assertThat(buffer.styleAt(x, y)).isEqualTo(Style.DEFAULT.fgColor(Color.indexed(x))); } } } @@ -65,7 +65,8 @@ public void testPanelView() { for (int x = 0; x < size.width(); x++) { assertThat(view.charAt(x, y)) .isEqualTo((char) ('G' + x + y * buffer.size().width())); - assertThat(view.styleAt(x, y)).isEqualTo(Style.ofFgColor(Color.indexed(x + 1))); + assertThat(view.styleAt(x, y)) + .isEqualTo(Style.DEFAULT.fgColor(Color.indexed(x + 1))); } } } @@ -96,7 +97,8 @@ public void testPanelNestedView() { for (int x = 0; x < size.width(); x++) { assertThat(view2.charAt(x, y)) .isEqualTo((char) ('M' + x + y * buffer.size().width())); - assertThat(view2.styleAt(x, y)).isEqualTo(Style.ofFgColor(Color.indexed(x + 2))); + assertThat(view2.styleAt(x, y)) + .isEqualTo(Style.DEFAULT.fgColor(Color.indexed(x + 2))); } } } @@ -131,7 +133,8 @@ public void testPanelNestedViewMoved() { for (int x = 0; x < size.width(); x++) { assertThat(view2.charAt(x, y)) .isEqualTo((char) ('S' + x + y * buffer.size().width())); - assertThat(view2.styleAt(x, y)).isEqualTo(Style.ofFgColor(Color.indexed(x + 3))); + assertThat(view2.styleAt(x, y)) + .isEqualTo(Style.DEFAULT.fgColor(Color.indexed(x + 3))); } } } @@ -167,7 +170,7 @@ public void testPanelNestedViewMovedPartiallyOutside() { if (y == 0 && x == 0) { assertThat(view2.charAt(x, y)).isEqualTo('Y'); assertThat(view2.styleAt(x, y)) - .isEqualTo(Style.ofFgColor(Color.indexed(x + 4))); + .isEqualTo(Style.DEFAULT.fgColor(Color.indexed(x + 4))); } else { assertThat(view2.charAt(x, y)).isEqualTo(LineBuffer.REPLACEMENT_CHAR); assertThat(view2.styleAt(x, y)).isEqualTo(Style.UNSTYLED); diff --git a/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestLine.java b/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestLine.java index 651ca1a..1b032dc 100644 --- a/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestLine.java +++ b/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestLine.java @@ -12,7 +12,7 @@ public class TestLine { @Test public void testRenderSingleStyledSpan() { Line l = Line.of("A", Style.BOLD); - assertThat(l.toAnsiString()).isEqualTo(Ansi.STYLE_RESET + Ansi.style(Ansi.BOLD) + "A"); + assertThat(l.toAnsiString()).isEqualTo(Ansi.style(Ansi.BOLD) + "A"); } @Test @@ -21,13 +21,7 @@ public void testRenderMultipleSpans() { String ansi = l.toAnsiString(); assertThat(ansi) - .isEqualTo( - Ansi.STYLE_RESET - + "A" - + Ansi.style(Ansi.BOLD) - + "B" - + Ansi.style(Ansi.NORMAL) - + "C"); + .isEqualTo("A" + Ansi.style(Ansi.BOLD) + "B" + Ansi.style(Ansi.NORMAL) + "C"); } @Test diff --git a/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestLineBuffer.java b/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestLineBuffer.java index 1b5576e..e59232b 100644 --- a/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestLineBuffer.java +++ b/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestLineBuffer.java @@ -21,7 +21,7 @@ public void testStyledBufferPutGetChar() { } for (int i = 0; i < buffer.length(); i++) { assertThat(buffer.charAt(i)).isEqualTo((char) ('a' + i)); - assertThat(buffer.styleAt(i)).isEqualTo(Style.ITALIC); + assertThat(buffer.styleAt(i)).isEqualTo(Style.DEFAULT.italic()); } } @@ -57,7 +57,7 @@ public void testStyledBufferPutCharToAnsiStringWithCurrentStyle() { Style style = i < 5 ? Style.ITALIC : Style.UNDERLINED; buffer.setCharAt(i, style, (char) ('a' + i)); } - assertThat(buffer.toAnsiString(Style.ITALIC)) + assertThat(buffer.toAnsiString(Style.DEFAULT.italic())) .isEqualTo("abcde" + Ansi.style(Ansi.NOTITALICIZED, Ansi.UNDERLINED) + "fghij"); } @@ -80,10 +80,10 @@ public void testStyledBufferPutCharToAnsiStringWithUnderAndOverflow() { @Test public void testStyledBufferPutStringGetChar() { LineBuffer buffer = LineBuffer.of(10); - buffer.putStringAt(0, Style.ITALIC, "abcdefghij"); + buffer.putStringAt(0, Style.DEFAULT.italic(), "abcdefghij"); for (int i = 0; i < buffer.length(); i++) { assertThat(buffer.charAt(i)).isEqualTo((char) ('a' + i)); - assertThat(buffer.styleAt(i)).isEqualTo(Style.ITALIC); + assertThat(buffer.styleAt(i)).isEqualTo(Style.DEFAULT.italic()); } } } diff --git a/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestSpan.java b/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestSpan.java index a86976e..d8b9e71 100644 --- a/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestSpan.java +++ b/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestSpan.java @@ -32,6 +32,6 @@ public void testLengthZwjSequence() { @Test public void testSpansRender() { Span s = Span.of("A", Style.BOLD); - assertThat(s.toAnsiString()).isEqualTo(Ansi.STYLE_RESET + Ansi.style(Ansi.BOLD) + "A"); + assertThat(s.toAnsiString()).isEqualTo(Ansi.style(Ansi.BOLD) + "A"); } } diff --git a/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestText.java b/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestText.java index 8c6298d..9d9003f 100644 --- a/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestText.java +++ b/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestText.java @@ -2,7 +2,6 @@ import static org.assertj.core.api.Assertions.assertThat; -import org.codejive.twinkle.ansi.Ansi; import org.codejive.twinkle.ansi.Style; import org.codejive.twinkle.util.StyledIterator; import org.junit.jupiter.api.Test; @@ -12,21 +11,21 @@ public class TestText { @Test public void testOfSimpleString() { Text t = Text.of("Hello World"); - assertThat(t.toAnsiString()).isEqualTo(Ansi.STYLE_RESET + "Hello World"); + assertThat(t.toAnsiString()).isEqualTo("Hello World"); } @Test public void testOfStyledString() { Style style = Style.BOLD; Text t = Text.of("Hello World", style); - assertThat(t.toAnsiString(Style.UNSTYLED)).isEqualTo(style.toAnsiString() + "Hello World"); + assertThat(t.toAnsiString()).isEqualTo(style.toAnsiString() + "Hello World"); } @Test public void testOfStyleState() { Style style = Style.BOLD; Text t = Text.of("Hello World", style); - assertThat(t.toAnsiString(Style.UNSTYLED)).isEqualTo(style.toAnsiString() + "Hello World"); + assertThat(t.toAnsiString()).isEqualTo(style.toAnsiString() + "Hello World"); } @Test @@ -34,7 +33,7 @@ public void testOfLines() { Line line1 = Line.of("Line 1"); Line line2 = Line.of("Line 2"); Text t = Text.of(line1, line2); - assertThat(t.toAnsiString()).isEqualTo(Ansi.STYLE_RESET + "Line 1\nLine 2"); + assertThat(t.toAnsiString()).isEqualTo("Line 1\nLine 2"); } @Test From 7cbb12c341b508b701672e669c43875768e2a968 Mon Sep 17 00:00:00 2001 From: Tako Schotanus Date: Wed, 7 Jan 2026 19:11:45 +0100 Subject: [PATCH 2/2] chore: have LineBuffer skip cells when necessary --- .../twinkle/core/text/LineBuffer.java | 38 +++++++++++-------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/twinkle-core/src/main/java/org/codejive/twinkle/core/text/LineBuffer.java b/twinkle-core/src/main/java/org/codejive/twinkle/core/text/LineBuffer.java index 2325efd..d6f3256 100644 --- a/twinkle-core/src/main/java/org/codejive/twinkle/core/text/LineBuffer.java +++ b/twinkle-core/src/main/java/org/codejive/twinkle/core/text/LineBuffer.java @@ -133,9 +133,7 @@ private void setCharAt_(int index, @NonNull Style style, char ch) { // TODO log warning about surrogate characters not being supported ch = REPLACEMENT_CHAR; } - cpBuffer[index] = ch; - graphemeBuffer[index] = null; - styleBuffer[index] = style.state(); + setCharAt_(index, style.state(), ch, null); } @Override @@ -147,9 +145,7 @@ public void setCharAt(int index, @NonNull Style style, int cp) { } private void setCharAt_(int index, @NonNull Style style, int cp) { - cpBuffer[index] = cp; - graphemeBuffer[index] = null; - styleBuffer[index] = style.state(); + setCharAt_(index, style.state(), cp, null); } @Override @@ -164,9 +160,21 @@ private void setCharAt_(int index, @NonNull Style style, @NonNull CharSequence g if (grapheme.length() == 0) { return; } - cpBuffer[index] = -1; - graphemeBuffer[index] = grapheme.toString(); - styleBuffer[index] = style.state(); + setCharAt_(index, style.state(), -1, grapheme.toString()); + } + + private boolean shouldSkipAt(int index) { + return cpBuffer[index] == -1 && graphemeBuffer[index] == null && styleBuffer[index] == -1; + } + + private void setSkipAt(int index) { + setCharAt_(index, -1, -1, null); + } + + private void setCharAt_(int index, long styleState, int cp, String grapheme) { + cpBuffer[index] = cp; + graphemeBuffer[index] = grapheme; + styleBuffer[index] = styleState; } @Override @@ -214,12 +222,6 @@ public int putStringAt(int index, @NonNull StyledIterator iter) { return cnt; } - private void setSkipAt(int index) { - cpBuffer[index] = -1; - graphemeBuffer[index] = null; - styleBuffer[index] = -1; - } - @Override public @NonNull LineBufferImpl subSequence(int start, int end) { if (start < 0 || end > length() || start > end) { @@ -297,6 +299,9 @@ private boolean outside(int index, int length) { int initialCapacity = length(); StringBuilder sb = new StringBuilder(initialCapacity); for (int i = 0; i < length(); i++) { + if (shouldSkipAt(i)) { + continue; + } if (graphemeBuffer[i] != null) { sb.append(graphemeBuffer[i]); } else { @@ -331,6 +336,9 @@ private boolean outside(int index, int length) { appendable.append(Ansi.STYLE_RESET); } for (int i = 0; i < length(); i++) { + if (shouldSkipAt(i)) { + continue; + } if (styleBuffer[i] != currentStyle.state()) { Style style = Style.of(styleBuffer[i]); style.toAnsi(appendable, currentStyle);