From b298e118e313029ade533e9cf182b58be3d44862 Mon Sep 17 00:00:00 2001 From: Mike Cifelli Date: Wed, 22 Mar 2017 11:26:53 -0400 Subject: [PATCH] Add colors to the interactive interpreter --- .../InteractiveLispInterpreter.java | 6 +- src/interpreter/LispInterpreter.java | 2 +- src/main/LispMain.java | 26 ++-- src/terminal/ControlSequence.java | 14 ++ src/terminal/ControlSequenceHandler.java | 58 ++++++-- src/terminal/ControlSequenceLookup.java | 28 ++++ src/terminal/LispTerminal.java | 59 ++++---- src/terminal/SafeStream.java | 12 +- src/terminal/SelectGraphicRendition.java | 75 ++++++++++ test/terminal/ControlSequenceHandlerTest.java | 129 ++++++++++++++++++ test/terminal/ControlSequenceTest.java | 82 +++++++++++ test/terminal/LispTerminalTest.java | 8 +- 12 files changed, 435 insertions(+), 64 deletions(-) create mode 100644 src/terminal/ControlSequence.java create mode 100644 src/terminal/ControlSequenceLookup.java create mode 100644 src/terminal/SelectGraphicRendition.java create mode 100644 test/terminal/ControlSequenceHandlerTest.java create mode 100644 test/terminal/ControlSequenceTest.java diff --git a/src/interpreter/InteractiveLispInterpreter.java b/src/interpreter/InteractiveLispInterpreter.java index 0fda432..78bca67 100644 --- a/src/interpreter/InteractiveLispInterpreter.java +++ b/src/interpreter/InteractiveLispInterpreter.java @@ -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 diff --git a/src/interpreter/LispInterpreter.java b/src/interpreter/LispInterpreter.java index d06cf8f..34e9d95 100644 --- a/src/interpreter/LispInterpreter.java +++ b/src/interpreter/LispInterpreter.java @@ -31,7 +31,7 @@ public class LispInterpreter { protected void prompt() {} - private void evaluateAndPrintNextSExpression() { + protected void evaluateAndPrintNextSExpression() { try { evaluateAndPrintNextSExpressionWithException(); } catch (LispException e) { diff --git a/src/main/LispMain.java b/src/main/LispMain.java index 4ea277a..b03f644 100644 --- a/src/main/LispMain.java +++ b/src/main/LispMain.java @@ -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 makeSegmentDecorator(String color) { - return new Function() { - - @Override - public String apply(String s) { - return s + END_OF_SEGMENT; - } - }; - } - private void runWithFile(String fileName) { buildFileInterpreter(fileName).interpret(); } diff --git a/src/terminal/ControlSequence.java b/src/terminal/ControlSequence.java new file mode 100644 index 0000000..231aa3d --- /dev/null +++ b/src/terminal/ControlSequence.java @@ -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 {} +} \ No newline at end of file diff --git a/src/terminal/ControlSequenceHandler.java b/src/terminal/ControlSequenceHandler.java index 8e03194..74d2f33 100644 --- a/src/terminal/ControlSequenceHandler.java +++ b/src/terminal/ControlSequenceHandler.java @@ -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); } } diff --git a/src/terminal/ControlSequenceLookup.java b/src/terminal/ControlSequenceLookup.java new file mode 100644 index 0000000..98fdf7b --- /dev/null +++ b/src/terminal/ControlSequenceLookup.java @@ -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> controlSequenceMap = new HashMap<>(); + + static { + Map 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 commandMap = controlSequenceMap.getOrDefault(command, new HashMap<>()); + + return commandMap.getOrDefault(code, new NullControlSequence()); + } + +} \ No newline at end of file diff --git a/src/terminal/LispTerminal.java b/src/terminal/LispTerminal.java index fbee282..5b95fa4 100644 --- a/src/terminal/LispTerminal.java +++ b/src/terminal/LispTerminal.java @@ -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(); diff --git a/src/terminal/SafeStream.java b/src/terminal/SafeStream.java index e17994a..3fb05b9 100644 --- a/src/terminal/SafeStream.java +++ b/src/terminal/SafeStream.java @@ -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; } diff --git a/src/terminal/SelectGraphicRendition.java b/src/terminal/SelectGraphicRendition.java new file mode 100644 index 0000000..aa71a4c --- /dev/null +++ b/src/terminal/SelectGraphicRendition.java @@ -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'; + +} \ No newline at end of file diff --git a/test/terminal/ControlSequenceHandlerTest.java b/test/terminal/ControlSequenceHandlerTest.java new file mode 100644 index 0000000..7c14f43 --- /dev/null +++ b/test/terminal/ControlSequenceHandlerTest.java @@ -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)); + } + +} diff --git a/test/terminal/ControlSequenceTest.java b/test/terminal/ControlSequenceTest.java new file mode 100644 index 0000000..a6dbe2d --- /dev/null +++ b/test/terminal/ControlSequenceTest.java @@ -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 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")); + } + +} diff --git a/test/terminal/LispTerminalTest.java b/test/terminal/LispTerminalTest.java index fddd166..9b5aefb 100644 --- a/test/terminal/LispTerminalTest.java +++ b/test/terminal/LispTerminalTest.java @@ -507,5 +507,11 @@ public class LispTerminalTest { assertCursorPosition(0, 0); 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' } }); + } + }