Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@
<version>${google-java-format.version}</version>
<style>AOSP</style>
</googleJavaFormat>
<formatAnnotations />
<formatAnnotations/>
</java>
</configuration>
<executions>
Expand All @@ -91,8 +91,9 @@
<configuration>
<source>8</source>
<target>8</target>
<testSource>11</testSource>
<testTarget>11</testTarget> </configuration>
<testSource>15</testSource>
<testTarget>15</testTarget>
</configuration>
</plugin>
</plugins>
<pluginManagement>
Expand Down
152 changes: 150 additions & 2 deletions twinkle-ansi/src/main/java/org/codejive/twinkle/ansi/Style.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.codejive.twinkle.util.Printable;
import org.jspecify.annotations.NonNull;

public class Style implements Printable {
Expand All @@ -18,6 +19,7 @@ public class Style implements Printable {
private static final long IDX_CROSSEDOUT = 7;
// private static final long DOUBLEUNDERLINE = 8;

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;
Expand Down Expand Up @@ -229,7 +231,7 @@ public boolean isStrikethrough() {
}

public @NonNull Style fgColor(@NonNull Color color) {
long newState = (state & ~MASK_FG_COLOR) | (encodeColor(color) << SHIFT_FG_COLOR);
long newState = applyFgColor(state, color);
return of(newState);
}

Expand All @@ -239,7 +241,7 @@ public boolean isStrikethrough() {
}

public @NonNull Style bgColor(@NonNull Color color) {
long newState = (state & ~MASK_BG_COLOR) | (encodeColor(color) << SHIFT_BG_COLOR);
long newState = applyBgColor(state, color);
return of(newState);
}

Expand Down Expand Up @@ -315,6 +317,140 @@ 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) {
if (!ansiSequence.startsWith(Ansi.CSI) || !ansiSequence.endsWith("m")) {
return currentStyleState;
}

String content = ansiSequence.substring(2, ansiSequence.length() - 1);
String[] parts = content.split("[;:]", -1);
int[] codes = new int[parts.length];
for (int i = 0; i < parts.length; i++) {
try {
// Empty parameters are assumed to be 0 otherwise parse as integer
codes[i] = parts[i].isEmpty() ? 0 : Integer.parseInt(parts[i]);
} catch (NumberFormatException e) {
codes[i] = -1; // Invalid code, will be ignored
}
}

long state = currentStyleState;
for (int i = 0; i < codes.length; i++) {
int code = codes[i];
switch (code) {
case -1:
// Invalid code, ignore
break;
case 0:
state = 0;
break;
case 1:
state |= F_BOLD;
break;
case 2:
state |= F_FAINT;
break;
case 3:
state |= F_ITALIC;
break;
case 4:
state |= F_UNDERLINED;
break;
case 5:
state |= F_BLINK;
break;
case 7:
state |= F_INVERSE;
break;
case 8:
state |= F_HIDDEN;
break;
case 9:
state |= F_STRIKETHROUGH;
break;
case 22:
state &= ~(F_BOLD | F_FAINT);
break;
case 23:
state &= ~F_ITALIC;
break;
case 24:
state &= ~F_UNDERLINED;
break;
case 25:
state &= ~F_BLINK;
break;
case 27:
state &= ~F_INVERSE;
break;
case 28:
state &= ~F_HIDDEN;
break;
case 29:
state &= ~F_STRIKETHROUGH;
break;
case 39:
state &= ~MASK_FG_COLOR;
break;
case 49:
state &= ~MASK_BG_COLOR;
break;
default:
if (code >= 30 && code <= 37) {
Color c = Color.basic(code - 30, Color.BasicColor.Intensity.normal);
state = applyFgColor(state, c);
} else if (code >= 90 && code <= 97) {
Color c = Color.basic(code - 90, Color.BasicColor.Intensity.bright);
state = applyFgColor(state, c);
} else if (code >= 40 && code <= 47) {
Color c = Color.basic(code - 40, Color.BasicColor.Intensity.normal);
state = applyBgColor(state, c);
} else if (code >= 100 && code <= 107) {
Color c = Color.basic(code - 100, Color.BasicColor.Intensity.bright);
state = applyBgColor(state, c);
} else if (code == 38 || code == 48) {
boolean isFg = (code == 38);
if (i + 1 < codes.length) {
int type = codes[i + 1];
if (type == 5 && i + 2 < codes.length) {
Color c = Color.indexed(codes[i + 2]);
if (isFg) {
state = applyFgColor(state, c);
} else {
state = applyBgColor(state, 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);
} else {
state = applyBgColor(state, c);
}
i += 4;
}
}
}
break;
}
}
return state;
}

private static long applyFgColor(long state, Color color) {
long encoded = encodeColor(color);
return (state & ~MASK_FG_COLOR) | (encoded << SHIFT_FG_COLOR);
}

private static long applyBgColor(long state, Color color) {
long encoded = encodeColor(color);
return (state & ~MASK_BG_COLOR) | (encoded << SHIFT_BG_COLOR);
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
Expand All @@ -332,6 +468,10 @@ public int hashCode() {
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("StyleImpl{");
if (state == F_UNKNOWN) {
sb.append("UNKNOWN}");
return sb.toString();
}
if (isBold()) sb.append("bold, ");
if (isFaint()) sb.append("faint, ");
if (isItalic()) sb.append("italic, ");
Expand Down Expand Up @@ -374,6 +514,14 @@ public String toString() {
@Override
public @NonNull Appendable toAnsi(Appendable appendable, long currentStyleState)
throws IOException {
if (state == F_UNKNOWN) {
// Do nothing, we keep the current state
return appendable;
}
if (currentStyleState == F_UNKNOWN) {
appendable.append(Ansi.STYLE_RESET);
currentStyleState = F_UNSTYLED;
}
List<Object> 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
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.codejive.twinkle.ansi;
package org.codejive.twinkle.util;

import java.io.IOException;
import org.codejive.twinkle.ansi.Style;
import org.jspecify.annotations.NonNull;

public interface Printable {
Expand All @@ -10,7 +11,9 @@ public interface Printable {
*
* @return The ANSI string representation of the object.
*/
@NonNull String toAnsiString();
default @NonNull String toAnsiString() {
return toAnsiString(Style.F_UNKNOWN);
}

/**
* Outputs the object as an ANSI string, including ANSI escape codes for styles. This method
Expand All @@ -19,7 +22,9 @@ public interface Printable {
* @param appendable The <code>Appendable</code> to write the ANSI output to.
* @return The <code>Appendable</code> passed as parameter.
*/
@NonNull Appendable toAnsi(Appendable appendable) throws IOException;
default @NonNull Appendable toAnsi(Appendable appendable) throws IOException {
return toAnsi(appendable, Style.F_UNKNOWN);
}

/**
* Converts the object to an ANSI string, including ANSI escape codes for styles. This method
Expand Down Expand Up @@ -55,7 +60,14 @@ public interface Printable {
* @param currentStyleState The current style to start with.
* @return The ANSI string representation of the object.
*/
@NonNull String toAnsiString(long currentStyleState);
default @NonNull String toAnsiString(long currentStyleState) {
StringBuilder sb = new StringBuilder();
try {
return toAnsi(sb, currentStyleState).toString();
} catch (IOException e) {
throw new RuntimeException(e);
}
}

/**
* Outputs the object as an ANSI string, including ANSI escape codes for styles. This method
Expand Down
Loading
Loading