Add colors to the interactive interpreter

This commit is contained in:
Mike Cifelli 2017-03-22 11:26:53 -04:00
parent 058e937c3e
commit b298e118e3
12 changed files with 435 additions and 64 deletions

View File

@ -1,7 +1,5 @@
package interpreter;
import sexpression.SExpression;
public class InteractiveLispInterpreter extends LispInterpreter {
private static final String PROMPT = "~ ";
@ -13,9 +11,9 @@ public class InteractiveLispInterpreter extends LispInterpreter {
}
@Override
protected void printSExpression(SExpression sExpression) {
protected void evaluateAndPrintNextSExpression() {
environment.getOutput().println();
super.printSExpression(sExpression);
super.evaluateAndPrintNextSExpression();
}
@Override

View File

@ -31,7 +31,7 @@ public class LispInterpreter {
protected void prompt() {}
private void evaluateAndPrintNextSExpression() {
protected void evaluateAndPrintNextSExpression() {
try {
evaluateAndPrintNextSExpressionWithException();
} catch (LispException e) {

View File

@ -9,7 +9,7 @@ import com.googlecode.lanterna.terminal.*;
import interpreter.*;
import terminal.LispTerminal;
import terminal.SafeStream.SafePipedOutputStream;
import terminal.SafeStream.SafeOutputStream;
import terminal.SafeStream.UncheckedIOException;
public class LispMain {
@ -35,7 +35,7 @@ public class LispMain {
private PipedInputStream outputReader;
private PipedOutputStream outputWriter;
private PrintStream outputStream;
private SafePipedOutputStream safeOutputWriter;
private SafeOutputStream safeOutputWriter;
private LispTerminal lispTerminal;
private void runInteractive() {
@ -60,7 +60,7 @@ public class LispMain {
outputReader = new PipedInputStream();
outputWriter = new PipedOutputStream(outputReader);
outputStream = new PrintStream(outputWriter);
safeOutputWriter = new SafePipedOutputStream(outputWriter);
safeOutputWriter = new SafeOutputStream(outputWriter);
lispTerminal = new LispTerminal(createIOSafeTerminal(), inputWriter, outputReader);
}
@ -82,11 +82,11 @@ public class LispMain {
builder.setErrorOutput(outputStream);
builder.setTerminationFunction(this::shutdown);
builder.setErrorTerminationFunction(this::shutdown);
builder.setPromptDecorator(makeSegmentDecorator(ANSI_GREEN));
builder.setValueOutputDecorator(s -> s);
builder.setWarningOutputDecorator(s -> s);
builder.setErrorOutputDecorator(s -> s);
builder.setCriticalOutputDecorator(s -> s);
builder.setPromptDecorator(s -> s + END_OF_SEGMENT);
builder.setValueOutputDecorator(makeColorDecorator(ANSI_GREEN));
builder.setWarningOutputDecorator(makeColorDecorator(ANSI_YELLOW));
builder.setErrorOutputDecorator(makeColorDecorator(ANSI_RED));
builder.setCriticalOutputDecorator(makeColorDecorator(ANSI_PURPLE));
return builder.build();
}
@ -98,16 +98,6 @@ public class LispMain {
safeOutputWriter.close();
}
private static Function<String, String> makeSegmentDecorator(String color) {
return new Function<String, String>() {
@Override
public String apply(String s) {
return s + END_OF_SEGMENT;
}
};
}
private void runWithFile(String fileName) {
buildFileInterpreter(fileName).interpret();
}

View File

@ -0,0 +1,14 @@
package terminal;
import com.googlecode.lanterna.terminal.IOSafeTerminal;
public interface ControlSequence {
default String getCode() {
return "";
}
default void applyTo(IOSafeTerminal terminal) {}
public static class NullControlSequence implements ControlSequence {}
}

View File

@ -1,25 +1,63 @@
package terminal;
import static terminal.ControlSequenceHandler.Command.SGR;
import static java.lang.Character.isDigit;
import static terminal.ControlSequenceLookup.lookupControlSequence;
import static util.Characters.*;
import terminal.SafeStream.SafeInputStream;
class ControlSequenceHandler {
private static final char ESCAPE = '\u001B';
public static final boolean isEscape(char c) {
return c == '\u001B';
return c == ESCAPE;
}
private boolean inControlSequence;
private int code;
private Command command;
private SafeInputStream input;
private String code;
private int currentCharacter;
public ControlSequenceHandler() {
this.inControlSequence = false;
this.code = 0;
this.command = SGR;
this.input = null;
this.code = "";
this.currentCharacter = 0;
}
public static enum Command {
SGR
public ControlSequence parse(SafeInputStream inputStream) {
input = inputStream;
code = "";
readCharacter();
if (isExpectedFirstCharacter())
readCode();
return lookupControlSequence((char) currentCharacter, code);
}
private void readCharacter() {
currentCharacter = input.read();
}
private boolean isExpectedFirstCharacter() {
return isCharacter() && isLeftBracket();
}
private boolean isCharacter() {
return currentCharacter != EOF;
}
private boolean isLeftBracket() {
return (char) currentCharacter == LEFT_SQUARE_BRACKET;
}
private void readCode() {
for (readCharacter(); isPartOfCode(); readCharacter())
code += (char) currentCharacter;
}
private boolean isPartOfCode() {
return isCharacter() && isDigit((char) currentCharacter);
}
}

View File

@ -0,0 +1,28 @@
package terminal;
import static terminal.SelectGraphicRendition.SGR_COMMAND;
import java.util.*;
import terminal.ControlSequence.NullControlSequence;
public class ControlSequenceLookup {
private static Map<Character, Map<String, ControlSequence>> controlSequenceMap = new HashMap<>();
static {
Map<String, ControlSequence> selectGraphicRenditionMap = new HashMap<>();
for (SelectGraphicRendition sgr : SelectGraphicRendition.values())
selectGraphicRenditionMap.put(sgr.getCode(), sgr);
controlSequenceMap.put(SGR_COMMAND, selectGraphicRenditionMap);
}
public static ControlSequence lookupControlSequence(char command, String code) {
Map<String, ControlSequence> commandMap = controlSequenceMap.getOrDefault(command, new HashMap<>());
return commandMap.getOrDefault(code, new NullControlSequence());
}
}

View File

@ -18,10 +18,10 @@ public class LispTerminal {
public static final char END_OF_SEGMENT = 'x';
private IOSafeTerminal terminal;
private SafePipedOutputStream inputWriter;
private SafePipedInputStream outputReader;
private ExecutorService executorService;
private SafeOutputStream inputWriter;
private SafeInputStream outputReader;
private ControlSequenceHandler controlSequenceHandler;
private ExecutorService executorService;
private TerminalSize terminalSize;
private String inputLine;
private String outputSegment;
@ -31,20 +31,20 @@ public class LispTerminal {
public LispTerminal(IOSafeTerminal terminal, PipedOutputStream inputWriter, PipedInputStream outputReader) {
this.terminal = terminal;
this.inputWriter = new SafePipedOutputStream(inputWriter);
this.outputReader = new SafePipedInputStream(outputReader);
this.executorService = Executors.newFixedThreadPool(2);
this.inputWriter = new SafeOutputStream(inputWriter);
this.outputReader = new SafeInputStream(outputReader);
this.controlSequenceHandler = new ControlSequenceHandler();
this.executorService = Executors.newFixedThreadPool(2);
this.terminalSize = terminal.getTerminalSize();
this.inputLine = "";
this.outputSegment = "";
this.isStopped = false;
updateOrigin();
setOriginToCurrentPosition();
terminal.addResizeListener((t, newSize) -> resize());
}
private synchronized void updateOrigin() {
private synchronized void setOriginToCurrentPosition() {
TerminalPosition cursorPosition = terminal.getCursorPosition();
originColumn = cursorPosition.getColumn();
originRow = cursorPosition.getRow();
@ -54,12 +54,12 @@ public class LispTerminal {
terminalSize = terminal.getTerminalSize();
terminal.clearScreen();
terminal.setCursorPosition(0, 0);
updateOrigin();
putString(inputLine);
setOriginToCurrentPosition();
putStringToTerminal(inputLine);
moveCursorToEndOfInput();
}
private synchronized void putString(String characters) {
private synchronized void putStringToTerminal(String characters) {
for (char c : characters.toCharArray())
terminal.putCharacter(c);
}
@ -113,7 +113,7 @@ public class LispTerminal {
private void doControlC() {
moveCursorToEndOfInput();
terminal.putCharacter('\n');
updateOrigin();
setOriginToCurrentPosition();
stop();
}
@ -170,7 +170,7 @@ public class LispTerminal {
inputWriter.write(inputLine.getBytes());
inputWriter.flush();
inputLine = "";
updateOrigin();
setOriginToCurrentPosition();
}
private synchronized void doLeftArrow() {
@ -257,7 +257,7 @@ public class LispTerminal {
String remaining = inputLine.substring(distanceFromOrigin, inputLine.length());
inputLine = inputLine.substring(0, distanceFromOrigin - 1) + remaining;
moveCursorLeft(cursorPosition);
putString(remaining + " ");
putStringToTerminal(remaining + " ");
moveCursorLeft(cursorPosition);
}
@ -272,7 +272,7 @@ public class LispTerminal {
int distanceFromOrigin = getDistanceFromOrigin(cursorPosition);
String remaining = inputLine.substring(distanceFromOrigin + 1, inputLine.length());
inputLine = inputLine.substring(0, distanceFromOrigin) + remaining;
putString(remaining + " ");
putStringToTerminal(remaining + " ");
terminal.setCursorPosition(cursorPosition);
}
@ -299,7 +299,7 @@ public class LispTerminal {
int distanceFromOrigin = getDistanceFromOrigin(cursorPosition);
String remaining = character + inputLine.substring(distanceFromOrigin, inputLine.length());
inputLine = inputLine.substring(0, distanceFromOrigin) + remaining;
putString(remaining);
putStringToTerminal(remaining);
moveCursorRight(cursorPosition);
}
@ -316,7 +316,7 @@ public class LispTerminal {
private synchronized TerminalPosition adjustCursorPosition(TerminalPosition cursorPosition) {
terminal.setCursorPosition(new TerminalPosition(originColumn, originRow));
putString(inputLine);
putStringToTerminal(inputLine);
return cursorPosition.withRelativeRow(-1);
}
@ -343,16 +343,12 @@ public class LispTerminal {
}
private synchronized void processOutput(char c) {
if (isEscape(c))
parseControlSequence();
else if (isEndOfSegment(c))
if (isEndOfSegment(c))
writeSegment();
else
outputSegment += c;
}
private synchronized void parseControlSequence() {}
private synchronized boolean isEndOfSegment(char c) {
return c == END_OF_SEGMENT;
}
@ -362,16 +358,31 @@ public class LispTerminal {
printSegmentCharacters();
terminal.setCursorVisible(true);
outputSegment = "";
updateOrigin();
setOriginToCurrentPosition();
}
private synchronized void printSegmentCharacters() {
moveCursorToEndOfInput();
putString(outputSegment);
putOutputToTerminal();
moveCursorToNextRowIfNecessary();
terminal.flush();
}
private synchronized void putOutputToTerminal() {
SafeInputStream input = new SafeInputStream(new ByteArrayInputStream(outputSegment.getBytes()));
for (int c = input.read(); c != EOF; c = input.read())
if (isEscape((char) c))
applyControlSequence(input);
else
terminal.putCharacter((char) c);
}
private void applyControlSequence(SafeInputStream input) {
ControlSequence controlSequence = controlSequenceHandler.parse(input);
controlSequence.applyTo(terminal);
}
private synchronized void moveCursorToNextRowIfNecessary() {
TerminalPosition cursorPosition = terminal.getCursorPosition();

View File

@ -4,11 +4,11 @@ import java.io.*;
public interface SafeStream {
public static class SafePipedInputStream {
public static class SafeInputStream {
private PipedInputStream underlyingStream;
private InputStream underlyingStream;
public SafePipedInputStream(PipedInputStream underlyingStream) {
public SafeInputStream(InputStream underlyingStream) {
this.underlyingStream = underlyingStream;
}
@ -29,11 +29,11 @@ public interface SafeStream {
}
}
public static class SafePipedOutputStream {
public static class SafeOutputStream {
private PipedOutputStream underlyingStream;
private OutputStream underlyingStream;
public SafePipedOutputStream(PipedOutputStream underlyingStream) {
public SafeOutputStream(OutputStream underlyingStream) {
this.underlyingStream = underlyingStream;
}

View File

@ -0,0 +1,75 @@
package terminal;
import com.googlecode.lanterna.TextColor;
import com.googlecode.lanterna.terminal.IOSafeTerminal;
public enum SelectGraphicRendition implements ControlSequence {
RESET {
@Override
public String getCode() {
return "0";
}
@Override
public void applyTo(IOSafeTerminal terminal) {
terminal.resetColorAndSGR();
}
},
RED {
@Override
public String getCode() {
return "31";
}
@Override
public void applyTo(IOSafeTerminal terminal) {
terminal.setForegroundColor(TextColor.ANSI.RED);
}
},
GREEN {
@Override
public String getCode() {
return "32";
}
@Override
public void applyTo(IOSafeTerminal terminal) {
terminal.setForegroundColor(TextColor.ANSI.GREEN);
}
},
YELLOW {
@Override
public String getCode() {
return "33";
}
@Override
public void applyTo(IOSafeTerminal terminal) {
terminal.setForegroundColor(TextColor.ANSI.YELLOW);
}
},
PURPLE {
@Override
public String getCode() {
return "35";
}
@Override
public void applyTo(IOSafeTerminal terminal) {
terminal.setForegroundColor(TextColor.ANSI.MAGENTA);
}
};
public static final char SGR_COMMAND = 'm';
}

View File

@ -0,0 +1,129 @@
package terminal;
import static org.junit.Assert.*;
import static terminal.ControlSequenceHandler.isEscape;
import static terminal.SelectGraphicRendition.*;
import org.junit.*;
import terminal.ControlSequence.NullControlSequence;
import terminal.SafeStream.SafeInputStream;
import testutil.TestUtilities;
import util.Characters;
public class ControlSequenceHandlerTest {
private ControlSequenceHandler handler;
private Object readRemaining(SafeInputStream input) {
String remaining = "";
for (int c = input.read(); c != Characters.EOF; c = input.read())
remaining += (char) c;
return remaining;
}
private SafeInputStream createSafeInputStream(String data) {
return new SafeInputStream(TestUtilities.createInputStreamFromString(data));
}
@Before
public void setUp() {
handler = new ControlSequenceHandler();
}
@Test
public void isEscapeDetectsNonEscapeCharacter() {
assertFalse(isEscape('x'));
}
@Test
public void isEscapeDetectsEscapeCharacter() {
assertTrue(isEscape('\u001b'));
}
@Test
public void correctlyParsesControlSequence_LeavesRestOfStreamIntact() {
SafeInputStream input = createSafeInputStream("[32mdata");
handler.parse(input);
assertEquals("data", readRemaining(input));
}
@Test
public void unterminatedControlSequence_OnlyConsumesFirstNonSequenceCharacter() {
SafeInputStream input = createSafeInputStream("[32data");
handler.parse(input);
assertEquals("ata", readRemaining(input));
}
@Test
public void malformedControlSequence_OnlyConsumesOneCharacter() {
SafeInputStream input = createSafeInputStream("32mdata");
handler.parse(input);
assertEquals("2mdata", readRemaining(input));
}
@Test
public void parsedControlSequenceIsCorrectType_EOF() {
SafeInputStream input = createSafeInputStream("");
assertTrue(handler.parse(input) instanceof NullControlSequence);
}
@Test
public void parsedControlSequenceIsCorrectType_EOF_AfterFirstCharacter() {
SafeInputStream input = createSafeInputStream("[");
assertTrue(handler.parse(input) instanceof NullControlSequence);
}
@Test
public void parsedControlSequenceIsCorrectType_UnterminatedControlSequence() {
SafeInputStream input = createSafeInputStream("[data");
assertTrue(handler.parse(input) instanceof NullControlSequence);
}
@Test
public void parsedControlSequenceIsCorrectType_MalformedControlSequence() {
SafeInputStream input = createSafeInputStream("32mdata");
assertTrue(handler.parse(input) instanceof NullControlSequence);
}
@Test
public void parsedControlSequenceIsCorrectType_SGR_Reset() {
SafeInputStream input = createSafeInputStream("[0m");
assertEquals(RESET, handler.parse(input));
}
@Test
public void parsedControlSequenceIsCorrectType_SGR_Red() {
SafeInputStream input = createSafeInputStream("[31m");
assertEquals(RED, handler.parse(input));
}
@Test
public void parsedControlSequenceIsCorrectType_SGR_Green() {
SafeInputStream input = createSafeInputStream("[32m");
assertEquals(GREEN, handler.parse(input));
}
@Test
public void parsedControlSequenceIsCorrectType_SGR_Yellow() {
SafeInputStream input = createSafeInputStream("[33m");
assertEquals(YELLOW, handler.parse(input));
}
@Test
public void parsedControlSequenceIsCorrectType_SGR_Purple() {
SafeInputStream input = createSafeInputStream("[35m");
assertEquals(PURPLE, handler.parse(input));
}
@Test
public void parseMultipleControlSequences() {
SafeInputStream input = createSafeInputStream("[35m[32m[0m");
assertEquals(PURPLE, handler.parse(input));
assertEquals(GREEN, handler.parse(input));
assertEquals(RESET, handler.parse(input));
}
}

View File

@ -0,0 +1,82 @@
package terminal;
import static org.junit.Assert.assertTrue;
import static terminal.SelectGraphicRendition.*;
import java.util.*;
import org.junit.*;
import com.googlecode.lanterna.TextColor;
import com.googlecode.lanterna.terminal.virtual.*;
import terminal.ControlSequence.NullControlSequence;
public class ControlSequenceTest {
private Set<String> indicatorSet;
private VirtualTerminal createTerminalWithIndicators() {
return new DefaultVirtualTerminal() {
@Override
public void resetColorAndSGR() {
indicatorSet.add("RESET");
}
@Override
public void setForegroundColor(TextColor color) {
indicatorSet.add(color.toString());
}
};
}
@Before
public void setUp() {
indicatorSet = new HashSet<>();
}
@Test
public void nullControlSequenceDoesNothing() {
ControlSequence nullControlSequence = new NullControlSequence();
VirtualTerminal terminal = createTerminalWithIndicators();
nullControlSequence.applyTo(terminal);
assertTrue(indicatorSet.isEmpty());
}
@Test
public void controlSequenceUpdatesTerminal_SGR_Reset() {
VirtualTerminal terminal = createTerminalWithIndicators();
RESET.applyTo(terminal);
assertTrue(indicatorSet.contains("RESET"));
}
@Test
public void controlSequenceUpdatesTerminal_SGR_Red() {
VirtualTerminal terminal = createTerminalWithIndicators();
RED.applyTo(terminal);
assertTrue(indicatorSet.contains("RED"));
}
@Test
public void controlSequenceUpdatesTerminal_SGR_Green() {
VirtualTerminal terminal = createTerminalWithIndicators();
GREEN.applyTo(terminal);
assertTrue(indicatorSet.contains("GREEN"));
}
@Test
public void controlSequenceUpdatesTerminal_SGR_Yellow() {
VirtualTerminal terminal = createTerminalWithIndicators();
YELLOW.applyTo(terminal);
assertTrue(indicatorSet.contains("YELLOW"));
}
@Test
public void controlSequenceUpdatesTerminal_SGR_Purple() {
VirtualTerminal terminal = createTerminalWithIndicators();
PURPLE.applyTo(terminal);
assertTrue(indicatorSet.contains("MAGENTA"));
}
}

View File

@ -508,4 +508,10 @@ public class LispTerminalTest {
assertCharacterPositions(new char[][] { { ' ', ' ', ' ' }, { ' ', ' ', ' ' } });
}
@Test
public void controlSequenceIsNotPrinted() {
produceOutput("\u001B[32mcontrol\u001B[0mseq");
assertCharacterPositions(new char[][] { { 'c', 'o', 'n', 't', 'r', 'o', 'l', 's', 'e', 'q' } });
}
}