//
// ANSIEmulation.java
// Thud
//
// Copyright (c) 2001-2007 Anthony Parker & the THUD team.
// All rights reserved. See LICENSE.TXT for more information.
//
package net.sourceforge.btthud.engine;
import java.util.List;
import java.util.ArrayList;
import java.util.regex.Pattern;
import java.util.regex.Matcher;
import java.awt.Color;
import javax.swing.text.AttributeSet;
import javax.swing.text.MutableAttributeSet;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.StyleConstants;
/**
* Emulates an ANSI terminal. Generates a list of Style name/String pairs,
* suitable for adding to a DefaultStyledDocument.
*
* The Style names conform to the following format (regexp-style):
*
* ANSI:f?h?u?i?[xrgybmcw]?[XRGYBMCW]?
*
* The characters after "ANSI:" have the following meanings:
*
* f - flash i - inverse
* h - hilite n - normal
* u - underline
*
* x - black foreground X - black background
* r - red foreground R - red background
* g - green foreground G - green background
* y - yellow foreground Y - yellow background
* b - blue foreground B - blue background
* m - magenta foreground M - magenta background
* c - cyan foreground C - cyan background
* w - white foreground W - white background
*
* The "n" (normal) code has no explicit encoding; it's just the "ANSI:" name.
*
* This encoding assigns one unique ANSI:-prefixed name for any particular
* emulator state. The intent is that a cache will be checked for existing
* Styles, and getStyle() used to generate attribute sets from names as needed.
*/
class ANSIEmulation {
// Escape sequence state constants.
static private enum EscapeState {
UNESCAPED, // waiting for ESC
ESCAPE_0, // waiting for [
ESCAPED // in escape
}
// Container for style/text pair.
static class StyledString {
public final String style;
public final String text;
private StyledString (final String style, final String text) {
this.style = style;
this.text = text;
}
private StyledString (final ANSIState state,
final CharSequence text) {
this(state.toString(), text.toString());
}
}
static private final Pattern stylePattern = Pattern.compile("ANSI:(f)?(h)?(u)?(i)?([xrgybmcw])?([XRGYBMCW])?");
//
// Parser.
//
private ANSIState termState = new ANSIState ();
// All ANSI terminal codes are of the form
//
// ESC [ <value> ; <value> ; ... <letter>
//
// though the only ones we're interested in are when <letter> = 'm'.
//
// We'll also leave an ANSI sequence if we encounter a line boundary.
// These should never occur anyway, and act as a safety mechanism.
//
// TODO: We could probably do this using regexps and some fancy regexp
// class, rather than writing a character parser by hand. A regexp
// engine would probably do it faster, too.
//
// The regular expression would be something like:
//
// \u001B \[ [^[:alpha:]]
List<StyledString> parseLine (final String line,
final boolean discard) {
// Initialize parser.
final List<StyledString> parsed;
final StringBuilder text;
ANSIState textState = new ANSIState (termState);
if (discard) {
parsed = null;
text = null;
} else {
parsed = new ArrayList<StyledString> ();
text = new StringBuilder ();
}
EscapeState escaped = EscapeState.UNESCAPED;
int start = 0; // start index of current parameters
int textStart = 0; // start index of current text
// Parse characters.
for (int ii = 0; ii < line.length(); ii++) {
final char ch = line.charAt(ii);
switch (escaped) {
case UNESCAPED: /* wait for ESC */
if (ch == '\u001B') {
escaped = EscapeState.ESCAPE_0;
}
break;
case ESCAPE_0: /* wait for [ */
if (ch == '[') {
escaped = EscapeState.ESCAPED;
start = ii + 1;
} else {
escaped = EscapeState.UNESCAPED;
}
break;
case ESCAPED: /* collect parameters */
if (!Character.isLetter(ch)) {
// Not done yet.
break;
}
if (!discard) {
final int textEnd = start - 2;
if (textStart != textEnd) {
// Buffer last state text.
if (!textState.equals(termState)) {
// Flush buffered text.
addStyledString(parsed, textState, text);
textState = new ANSIState (termState);
}
text.append(line,
textStart, textEnd);
}
textStart = ii + 1;
}
escaped = EscapeState.UNESCAPED;
if (ch != 'm') {
// Only handle 'm' command.
break;
}
updateFromParams(line.substring(start, ii));
break;
}
}
// Tidy up.
if (!discard) {
// Buffer last state text.
if (!textState.equals(termState)) {
// Flush buffered text.
addStyledString(parsed, textState, text);
textState = new ANSIState (termState);
}
text.append(line, textStart, line.length());
addStyledString(parsed, textState, text);
}
return parsed;
}
private void updateFromParams (final String paramString) {
final String[] params = paramString.split(";");
for (final String param: params) {
try {
termState.update(Integer.parseInt(param));
} catch (final NumberFormatException e) {
// Be generous and ignore bad param.
}
}
}
static private void addStyledString (final List<StyledString> strings,
final ANSIState textState,
final StringBuilder text) {
if (text.length() != 0) {
strings.add(new StyledString (textState, text));
text.setLength(0);
}
}
/**
* Style factory. Computes the attributes for a given ANSI style.
*/
// TODO: Actually set attributes.
static AttributeSet getStyle (final String name) {
// Parse style name.
final Matcher styleMatcher = stylePattern.matcher(name);
if (!styleMatcher.matches()) {
throw new IllegalArgumentException ("Bad ANSI style");
}
// Build up attribute set.
final MutableAttributeSet attrs = new SimpleAttributeSet ();
if (styleMatcher.group(1) != null) {
// Flash. Using italics instead of blinking.
StyleConstants.setItalic(attrs, true);
}
if (styleMatcher.group(2) != null) {
// Highlight.
StyleConstants.setBold(attrs, true);
}
if (styleMatcher.group(3) != null) {
// Underline.
StyleConstants.setUnderline(attrs, true);
}
// Handle foreground color.
Color fg = null;
if (styleMatcher.group(5) != null) {
fg = getColor(styleMatcher.group(5).charAt(0));
if (fg == Color.BLACK) {
fg = Color.GRAY;
}
}
// Handle background color.
Color bg = null;
if (styleMatcher.group(6) != null) {
final char bgChar = styleMatcher.group(6).charAt(0);
bg = getColor(Character.toLowerCase(bgChar));
}
// Invert colors, if necessary.
if (styleMatcher.group(4) != null) {
// Invert.
final Color fgOld = fg;
fg = bg;
bg = fgOld;
if (fg == null) {
// FIXME: Doing this correctly requires knowing
// what the current default background color
// is. For now, it's always black.
fg = Color.BLACK;
}
if (bg == null) {
// FIXME: Doing this correctly requires knowing
// what the current default foreground color
// is. For now, it's always white.
bg = Color.WHITE;
}
}
if (fg != null) {
StyleConstants.setForeground(attrs, fg);
}
if (bg != null) {
StyleConstants.setBackground(attrs, bg);
}
return attrs;
}
// Translate style character to color.
static private Color getColor (final char ch) {
switch (ch) {
case 'x': // black
return Color.BLACK;
case 'r': // red
return Color.RED;
case 'g': // green
return Color.GREEN;
case 'y': // yellow
return Color.YELLOW;
case 'b': // blue
return Color.BLUE;
case 'm': // magenta
return Color.MAGENTA;
case 'c': // cyan
return Color.CYAN;
case 'w': // white
return Color.WHITE;
default: // ???
throw new IllegalArgumentException ("Bad ANSI style");
}
}
// ANSI terminal state.
static private class ANSIState {
private boolean flash;
private boolean hilite;
private boolean underline;
private boolean inverse;
private char fg;
private char bg;
// Construct default ANSI state.
private ANSIState () {
reset();
}
// Construct a copy of an existing ANSI state.
private ANSIState (final ANSIState src) {
flash = src.flash;
hilite = src.hilite;
underline = src.underline;
inverse = src.inverse;
fg = src.fg;
bg = src.bg;
}
// Two ANSIStates are only equal if all values match.
public boolean equals (final Object o) {
if (!(o instanceof ANSIState)) {
return false;
}
final ANSIState that = (ANSIState)o;
if (flash != that.flash) return false;
if (hilite != that.hilite) return false;
if (underline != that.underline) return false;
if (inverse != that.inverse) return false;
if (fg != that.fg) return false;
if (bg != that.bg) return false;
return true;
}
// TODO: Implement hashCode(), if needed.
// Style name corresponding to this state.
public String toString () {
String name = "ANSI:";
if (flash) name += 'f';
if (hilite) name += 'h';
if (underline) name += 'u';
if (inverse) name += 'i';
if (fg != '?') name += fg;
if (bg != '?') name += bg;
return name;
}
// Update this state given an ANSI parameter value.
private void update (final int param) {
switch (param) {
case 0: // normal
reset();
break;
case 1: // highlight
hilite = true;
break;
case 4: // underline
underline = true;
break;
case 5: // flash
flash = true;
break;
case 7: // inverse
inverse = true;
break;
default:
if (param >= 30 && param <= 37) {
// Foreground color.
fg = getChar(param - 30);
} else if (param >= 40 && param <= 47) {
// Background color.
bg = getChar(param - 40);
bg = Character.toUpperCase(bg);
} else {
// Be generous and ignore bad param.
}
break;
}
}
private void reset () {
flash = false;
hilite = false;
underline = false;
inverse = false;
fg = '?';
bg = '?';
}
// Translate parameter to style character.
static private char getChar (final int param) {
switch (param) {
case 0: // black
return 'x';
case 1: // red
return 'r';
case 2: // green
return 'g';
case 3: // yellow
return 'y';
case 4: // blue
return 'b';
case 5: // magenta
return 'm';
case 6: // cyan
return 'c';
case 7: // white
return 'w';
default:
throw new IllegalArgumentException ("Bad color code");
}
}
}
}