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..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 @@ -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; @@ -132,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 @@ -146,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 @@ -163,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 @@ -213,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) { @@ -296,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 { @@ -325,7 +331,14 @@ 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 (shouldSkipAt(i)) { + continue; + } if (styleBuffer[i] != currentStyle.state()) { Style style = Style.of(styleBuffer[i]); style.toAnsi(appendable, currentStyle); 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