package terminal; import static com.googlecode.lanterna.input.KeyType.*; import static terminal.ControlSequenceHandler.isEscape; import static util.Characters.EOF; import static util.Characters.UNICODE_NULL; import java.io.*; import java.util.concurrent.*; import com.googlecode.lanterna.*; import com.googlecode.lanterna.input.*; import com.googlecode.lanterna.terminal.IOSafeTerminal; import stream.*; public class LispTerminal { public static final char END_OF_SEGMENT = UNICODE_NULL; private IOSafeTerminal terminal; private SafeOutputStream inputWriter; private SafeInputStream outputReader; private ControlSequenceHandler controlSequenceHandler; private ExecutorService executorService; private TerminalSize terminalSize; private String inputLine; private String outputSegment; private boolean isStopped; private int originColumn; private int originRow; public LispTerminal(IOSafeTerminal terminal, PipedOutputStream inputWriter, PipedInputStream outputReader) { this.terminal = terminal; 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; setOriginToCurrentPosition(); terminal.addResizeListener((t, newSize) -> resize()); } private synchronized void setOriginToCurrentPosition() { TerminalPosition cursorPosition = terminal.getCursorPosition(); originColumn = cursorPosition.getColumn(); originRow = cursorPosition.getRow(); } private synchronized void resize() { terminalSize = terminal.getTerminalSize(); terminal.clearScreen(); terminal.setCursorPosition(0, 0); redisplayInput(); } private synchronized void redisplayInput() { setOriginToCurrentPosition(); putStringToTerminal(inputLine); moveCursorToEndOfInput(); } private synchronized void putStringToTerminal(String characters) { for (char c : characters.toCharArray()) terminal.putCharacter(c); } private synchronized void moveCursorToEndOfInput() { terminal.setCursorPosition(getLeadingEdge()); } private synchronized TerminalPosition getLeadingEdge() { int inputLength = inputLine.length(); int totalColumns = terminalSize.getColumns(); int rowDifference = inputLength / totalColumns; int columnDifference = inputLength % totalColumns; return new TerminalPosition(originColumn + columnDifference, originRow + rowDifference); } public void start() { executorService.execute(this::readInput); executorService.execute(this::writeOutput); executorService.shutdown(); } private void readInput() { while (!isStopped) processNextKey(); } private void processNextKey() { KeyStroke keyStroke = getKeyStroke(); if (keyStroke != null) handleKey(keyStroke); else takeNap(); } private KeyStroke getKeyStroke() { KeyStroke keyStroke = null; // issue #299 try { keyStroke = terminal.pollInput(); } catch (IllegalStateException e) { doControlC(); } return keyStroke; } private synchronized void handleKey(KeyStroke keyStroke) { doKey(keyStroke); terminal.flush(); } private synchronized void doKey(KeyStroke keyStroke) { if (keyStroke.isCtrlDown()) doControlKey(keyStroke); else doNormalKey(keyStroke); } private synchronized void doControlKey(KeyStroke keyStroke) { KeyType keyType = keyStroke.getKeyType(); if (keyType == Character) doControlCharacter(keyStroke); } private synchronized void doControlCharacter(KeyStroke keyStroke) { if (keyStroke.getCharacter() == 'c') doControlC(); else if (keyStroke.getCharacter() == 'd') doControlD(); } private void doControlC() { moveCursorToEndOfInput(); terminal.putCharacter('\n'); inputLine = ""; setOriginToCurrentPosition(); stop(); } private synchronized void doControlD() { doEnter(); stop(); } private synchronized void doNormalKey(KeyStroke keyStroke) { KeyType keyType = keyStroke.getKeyType(); if (keyType == Enter) doEnter(); else if (keyType == ArrowLeft) doLeftArrow(); else if (keyType == ArrowRight) doRightArrow(); else if (keyType == Backspace) doBackspace(); else if (keyType == Delete) doDelete(); else if (keyType == Character) doCharacter(keyStroke.getCharacter()); } private synchronized void doEnter() { moveCursorToEndOfInput(); terminal.putCharacter('\n'); inputLine += "\n"; inputWriter.write(inputLine.getBytes()); inputWriter.flush(); inputLine = ""; setOriginToCurrentPosition(); } private synchronized void doLeftArrow() { TerminalPosition cursorPosition = terminal.getCursorPosition(); if (isPossibleToMoveLeft(cursorPosition)) moveCursorLeft(cursorPosition); } private synchronized boolean isPossibleToMoveLeft(TerminalPosition cursorPosition) { return getDistanceFromOrigin(cursorPosition) > 0; } private synchronized int getDistanceFromOrigin(TerminalPosition cursorPosition) { int columnDifference = cursorPosition.getColumn() - originColumn; int rowDifference = cursorPosition.getRow() - originRow; int totalColumns = terminalSize.getColumns(); return columnDifference + (totalColumns * rowDifference); } private synchronized void moveCursorLeft(TerminalPosition cursorPosition) { TerminalPosition newPosition = cursorPosition.withRelativeColumn(-1); if (isAtStartOfRow(cursorPosition)) newPosition = cursorPosition.withColumn(terminalSize.getColumns()).withRelativeRow(-1); terminal.setCursorPosition(newPosition); } private synchronized boolean isAtStartOfRow(TerminalPosition cursorPosition) { return cursorPosition.getColumn() == 0; } private synchronized void doRightArrow() { TerminalPosition cursorPosition = terminal.getCursorPosition(); if (isPossibleToMoveRight(cursorPosition)) moveCursorRight(cursorPosition); } private synchronized boolean isPossibleToMoveRight(TerminalPosition cursorPosition) { return getDistanceFromOrigin(cursorPosition) < inputLine.length(); } private synchronized void moveCursorRight(TerminalPosition cursorPosition) { if (isEndOfRow(cursorPosition)) moveCursorToNextRow(cursorPosition); else terminal.setCursorPosition(cursorPosition.withRelativeColumn(1)); } private synchronized boolean isEndOfRow(TerminalPosition cursorPosition) { return cursorPosition.getColumn() >= terminalSize.getColumns() - 1; } private synchronized void moveCursorToNextRow(TerminalPosition cursorPosition) { if (isEndOfBuffer(cursorPosition)) createNewRowForCursor(cursorPosition); else terminal.setCursorPosition(cursorPosition.withColumn(0).withRelativeRow(1)); } private synchronized boolean isEndOfBuffer(TerminalPosition cursorPosition) { return cursorPosition.getRow() == terminalSize.getRows() - 1; } private synchronized void createNewRowForCursor(TerminalPosition cursorPosition) { terminal.setCursorPosition(cursorPosition); terminal.putCharacter('\n'); terminal.setCursorPosition(cursorPosition.withColumn(0)); --originRow; } private synchronized void doBackspace() { TerminalPosition cursorPosition = terminal.getCursorPosition(); if (isPossibleToMoveLeft(cursorPosition)) deletePreviousCharacter(cursorPosition); } private synchronized void deletePreviousCharacter(TerminalPosition cursorPosition) { int distanceFromOrigin = getDistanceFromOrigin(cursorPosition); String remaining = inputLine.substring(distanceFromOrigin, inputLine.length()); inputLine = inputLine.substring(0, distanceFromOrigin - 1) + remaining; moveCursorLeft(cursorPosition); putStringToTerminal(remaining + " "); moveCursorLeft(cursorPosition); } private synchronized void doDelete() { TerminalPosition cursorPosition = terminal.getCursorPosition(); if (isPossibleToMoveRight(cursorPosition)) deleteCharacterAtPosition(cursorPosition); } private synchronized void deleteCharacterAtPosition(TerminalPosition cursorPosition) { int distanceFromOrigin = getDistanceFromOrigin(cursorPosition); String remaining = inputLine.substring(distanceFromOrigin + 1, inputLine.length()); inputLine = inputLine.substring(0, distanceFromOrigin) + remaining; putStringToTerminal(remaining + " "); terminal.setCursorPosition(cursorPosition); } private synchronized void doCharacter(Character character) { TerminalPosition cursorPosition = terminal.getCursorPosition(); if (!isBufferFilled()) if (isPossibleToMoveRight(cursorPosition)) insertCharacter(character, cursorPosition); else appendCharacter(character, cursorPosition); } private synchronized boolean isBufferFilled() { int row = getLeadingEdge().getRow(); int column = getLeadingEdge().getColumn(); return (row == terminalSize.getRows() - 1) && (column >= terminalSize.getColumns() - 1) && (originRow <= 0); } private synchronized void insertCharacter(Character character, TerminalPosition cursorPosition) { cursorPosition = shiftPositionIfNewRowAdded(cursorPosition); terminal.setCursorPosition(cursorPosition); int distanceFromOrigin = getDistanceFromOrigin(cursorPosition); String remaining = character + inputLine.substring(distanceFromOrigin, inputLine.length()); inputLine = inputLine.substring(0, distanceFromOrigin) + remaining; putStringToTerminal(remaining); moveCursorRight(cursorPosition); } private synchronized TerminalPosition shiftPositionIfNewRowAdded(TerminalPosition cursorPosition) { int oldOriginRow = originRow; moveCursorRight(getLeadingEdge()); return isNewRowAdded(oldOriginRow) ? adjustCursorPosition(cursorPosition) : cursorPosition; } private synchronized boolean isNewRowAdded(int oldOriginRow) { return originRow != oldOriginRow; } private synchronized TerminalPosition adjustCursorPosition(TerminalPosition cursorPosition) { terminal.setCursorPosition(new TerminalPosition(originColumn, originRow)); putStringToTerminal(inputLine); return cursorPosition.withRelativeRow(-1); } private synchronized void appendCharacter(Character character, TerminalPosition cursorPosition) { terminal.putCharacter(character); inputLine += character; moveCursorRight(cursorPosition); } private void takeNap() { try { Thread.sleep(1); } catch (InterruptedException ignored) {} } private void writeOutput() { for (int c = outputReader.read(); c != EOF; c = outputReader.read()) processOutput((char) c); terminal.setCursorVisible(true); terminal.flush(); terminal.close(); } private synchronized void processOutput(char c) { if (isEndOfSegment(c)) writeSegment(); else outputSegment += c; } private synchronized boolean isEndOfSegment(char c) { return c == END_OF_SEGMENT; } private synchronized void writeSegment() { terminal.setCursorVisible(false); printSegmentCharacters(); terminal.setCursorVisible(true); outputSegment = ""; redisplayInput(); terminal.flush(); } private synchronized void printSegmentCharacters() { moveCursorToEndOfInput(); putOutputToTerminal(); moveCursorToNextRowIfNecessary(); } private synchronized void putOutputToTerminal() { SafeInputStream input = convertOutputToStream(); for (int c = input.read(); c != EOF; c = input.read()) if (isEscape((char) c)) applyControlSequence(input); else terminal.putCharacter((char) c); } private synchronized SafeInputStream convertOutputToStream() { return new SafeInputStream(new ByteArrayInputStream(outputSegment.getBytes())); } private void applyControlSequence(SafeInputStream input) { ControlSequence controlSequence = controlSequenceHandler.parse(input); controlSequence.applyTo(terminal); } private synchronized void moveCursorToNextRowIfNecessary() { TerminalPosition cursorPosition = terminal.getCursorPosition(); if (isEndOfRow(cursorPosition)) moveCursorToNextRow(cursorPosition); } public void stop() { isStopped = true; inputWriter.close(); } }