/*
* This file is part of lanterna (http://code.google.com/p/lanterna/).
*
* lanterna is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see .
*
* Copyright (C) 2010-2015 Martin
*/
package com.googlecode.lanterna.terminal.ansi;
import com.googlecode.lanterna.SGR;
import com.googlecode.lanterna.input.*;
import com.googlecode.lanterna.TerminalSize;
import com.googlecode.lanterna.TextColor;
import com.googlecode.lanterna.terminal.ExtendedTerminal;
import com.googlecode.lanterna.terminal.MouseCaptureMode;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.Charset;
/**
* Class containing graphics code for ANSI compliant text terminals and terminal emulators. All the methods inside of
* this class uses ANSI escape codes written to the underlying output stream.
*
* @see Wikipedia
* @author Martin
*/
public abstract class ANSITerminal extends StreamBasedTerminal implements ExtendedTerminal {
private MouseCaptureMode mouseCaptureMode;
private boolean inPrivateMode;
@SuppressWarnings("WeakerAccess")
protected ANSITerminal(InputStream terminalInput, OutputStream terminalOutput, Charset terminalCharset) {
super(terminalInput, terminalOutput, terminalCharset);
this.inPrivateMode = false;
this.mouseCaptureMode = null;
getInputDecoder().addProfile(getDefaultKeyDecodingProfile());
}
/**
* This method can be overridden in a custom terminal implementation to change the default key decoders.
* @return The KeyDecodingProfile used by the terminal when translating character sequences to keystrokes
*/
protected KeyDecodingProfile getDefaultKeyDecodingProfile() {
return new DefaultKeyDecodingProfile();
}
private void writeCSISequenceToTerminal(byte... tail) throws IOException {
byte[] completeSequence = new byte[tail.length + 2];
completeSequence[0] = (byte)0x1b;
completeSequence[1] = (byte)'[';
System.arraycopy(tail, 0, completeSequence, 2, tail.length);
writeToTerminal(completeSequence);
}
private void writeSGRSequenceToTerminal(byte... sgrParameters) throws IOException {
byte[] completeSequence = new byte[sgrParameters.length + 3];
completeSequence[0] = (byte)0x1b;
completeSequence[1] = (byte)'[';
completeSequence[completeSequence.length - 1] = (byte)'m';
System.arraycopy(sgrParameters, 0, completeSequence, 2, sgrParameters.length);
writeToTerminal(completeSequence);
}
private void writeOSCSequenceToTerminal(byte... tail) throws IOException {
byte[] completeSequence = new byte[tail.length + 2];
completeSequence[0] = (byte)0x1b;
completeSequence[1] = (byte)']';
System.arraycopy(tail, 0, completeSequence, 2, tail.length);
writeToTerminal(completeSequence);
}
@Override
public TerminalSize getTerminalSize() throws IOException {
saveCursorPosition();
setCursorPosition(5000, 5000);
reportPosition();
restoreCursorPosition();
return waitForTerminalSizeReport();
}
@Override
public void setTerminalSize(int columns, int rows) throws IOException {
writeCSISequenceToTerminal(("8;" + rows + ";" + columns + "t").getBytes());
//We can't trust that the previous call was honoured by the terminal so force a re-query here, which will
//trigger a resize event if one actually took place
getTerminalSize();
}
@Override
public void setTitle(String title) throws IOException {
//The bell character is our 'null terminator', make sure there's none in the title
title = title.replace("\007", "");
writeOSCSequenceToTerminal(("2;" + title + "\007").getBytes());
}
@Override
public void setForegroundColor(TextColor color) throws IOException {
writeSGRSequenceToTerminal(color.getForegroundSGRSequence());
}
@Override
public void setBackgroundColor(TextColor color) throws IOException {
writeSGRSequenceToTerminal(color.getBackgroundSGRSequence());
}
@Override
public void enableSGR(SGR sgr) throws IOException {
switch(sgr) {
case BLINK:
writeCSISequenceToTerminal((byte) '5', (byte) 'm');
break;
case BOLD:
writeCSISequenceToTerminal((byte) '1', (byte) 'm');
break;
case BORDERED:
writeCSISequenceToTerminal((byte) '5', (byte) '1', (byte) 'm');
break;
case CIRCLED:
writeCSISequenceToTerminal((byte) '5', (byte) '2', (byte) 'm');
break;
case CROSSED_OUT:
writeCSISequenceToTerminal((byte) '9', (byte) 'm');
break;
case FRAKTUR:
writeCSISequenceToTerminal((byte) '2', (byte) '0', (byte) 'm');
break;
case REVERSE:
writeCSISequenceToTerminal((byte) '7', (byte) 'm');
break;
case UNDERLINE:
writeCSISequenceToTerminal((byte) '4', (byte) 'm');
break;
}
}
@Override
public void disableSGR(SGR sgr) throws IOException {
switch(sgr) {
case BLINK:
writeCSISequenceToTerminal((byte) '2', (byte) '5', (byte) 'm');
break;
case BOLD:
writeCSISequenceToTerminal((byte) '2', (byte) '2', (byte) 'm');
break;
case BORDERED:
writeCSISequenceToTerminal((byte) '5', (byte) '4', (byte) 'm');
break;
case CIRCLED:
writeCSISequenceToTerminal((byte) '5', (byte) '4', (byte) 'm');
break;
case CROSSED_OUT:
writeCSISequenceToTerminal((byte) '2', (byte) '9', (byte) 'm');
break;
case FRAKTUR:
writeCSISequenceToTerminal((byte) '2', (byte) '3', (byte) 'm');
break;
case REVERSE:
writeCSISequenceToTerminal((byte) '2', (byte) '7', (byte) 'm');
break;
case UNDERLINE:
writeCSISequenceToTerminal((byte) '2', (byte) '4', (byte) 'm');
break;
}
}
@Override
public void resetColorAndSGR() throws IOException {
writeCSISequenceToTerminal((byte) '0', (byte) 'm');
}
@Override
public void clearScreen() throws IOException {
writeCSISequenceToTerminal((byte) '2', (byte) 'J');
}
@Override
public void enterPrivateMode() throws IOException {
if(inPrivateMode) {
throw new IllegalStateException("Cannot call enterPrivateMode() when already in private mode");
}
writeCSISequenceToTerminal((byte) '?', (byte) '1', (byte) '0', (byte) '4', (byte) '9', (byte) 'h');
inPrivateMode = true;
}
@Override
public void exitPrivateMode() throws IOException {
if(!inPrivateMode) {
throw new IllegalStateException("Cannot call exitPrivateMode() when not in private mode");
}
resetColorAndSGR();
setCursorVisible(true);
writeCSISequenceToTerminal((byte) '?', (byte) '1', (byte) '0', (byte) '4', (byte) '9', (byte) 'l');
inPrivateMode = false;
}
@Override
public void setCursorPosition(int x, int y) throws IOException {
writeCSISequenceToTerminal(((y + 1) + ";" + (x + 1) + "H").getBytes());
}
@Override
public void setCursorVisible(boolean visible) throws IOException {
writeCSISequenceToTerminal(("?25" + (visible ? "h" : "l")).getBytes());
}
@Override
public KeyStroke readInput() throws IOException {
KeyStroke keyStroke;
do {
keyStroke = filterMouseEvents(super.readInput());
} while(keyStroke == null);
return keyStroke;
}
@Override
public KeyStroke pollInput() throws IOException {
return filterMouseEvents(super.pollInput());
}
private KeyStroke filterMouseEvents(KeyStroke keyStroke) {
//Remove bad input events from terminals that are not following the xterm protocol properly
if(keyStroke == null || keyStroke.getKeyType() != KeyType.MouseEvent) {
return keyStroke;
}
MouseAction mouseAction = (MouseAction)keyStroke;
switch(mouseAction.getActionType()) {
case CLICK_RELEASE:
if(mouseCaptureMode == MouseCaptureMode.CLICK) {
return null;
}
break;
case DRAG:
if(mouseCaptureMode == MouseCaptureMode.CLICK ||
mouseCaptureMode == MouseCaptureMode.CLICK_RELEASE) {
return null;
}
break;
case MOVE:
if(mouseCaptureMode == MouseCaptureMode.CLICK ||
mouseCaptureMode == MouseCaptureMode.CLICK_RELEASE ||
mouseCaptureMode == MouseCaptureMode.CLICK_RELEASE_DRAG) {
return null;
}
break;
default:
}
return mouseAction;
}
@Override
public void pushTitle() throws IOException {
throw new UnsupportedOperationException("Not implemented yet");
}
@Override
public void popTitle() throws IOException {
throw new UnsupportedOperationException("Not implemented yet");
}
@Override
public void iconify() throws IOException {
writeCSISequenceToTerminal((byte)'2', (byte)'t');
}
@Override
public void deiconify() throws IOException {
writeCSISequenceToTerminal((byte)'1', (byte)'t');
}
@Override
public void maximize() throws IOException {
writeCSISequenceToTerminal((byte)'9', (byte)';', (byte)'1', (byte)'t');
}
@Override
public void unmaximize() throws IOException {
writeCSISequenceToTerminal((byte)'9', (byte)';', (byte)'0', (byte)'t');
}
@Override
public void setMouseCaptureMode(MouseCaptureMode mouseCaptureMode) throws IOException {
if(this.mouseCaptureMode != null) {
switch(this.mouseCaptureMode) {
case CLICK:
writeCSISequenceToTerminal((byte)'?', (byte)'9', (byte)'l');
break;
case CLICK_RELEASE:
writeCSISequenceToTerminal((byte)'?', (byte)'1', (byte)'0', (byte)'0', (byte)'0', (byte)'l');
break;
case CLICK_RELEASE_DRAG:
writeCSISequenceToTerminal((byte)'?', (byte)'1', (byte)'0', (byte)'0', (byte)'2', (byte)'l');
break;
case CLICK_RELEASE_DRAG_MOVE:
writeCSISequenceToTerminal((byte)'?', (byte)'1', (byte)'0', (byte)'0', (byte)'3', (byte)'l');
break;
}
if(getCharset().equals(Charset.forName("UTF-8"))) {
writeCSISequenceToTerminal((byte)'?', (byte)'1', (byte)'0', (byte)'0', (byte)'5', (byte)'l');
}
}
this.mouseCaptureMode = mouseCaptureMode;
if(this.mouseCaptureMode != null) {
switch(this.mouseCaptureMode) {
case CLICK:
writeCSISequenceToTerminal((byte)'?', (byte)'9', (byte)'h');
break;
case CLICK_RELEASE:
writeCSISequenceToTerminal((byte)'?', (byte)'1', (byte)'0', (byte)'0', (byte)'0', (byte)'h');
break;
case CLICK_RELEASE_DRAG:
writeCSISequenceToTerminal((byte)'?', (byte)'1', (byte)'0', (byte)'0', (byte)'2', (byte)'h');
break;
case CLICK_RELEASE_DRAG_MOVE:
writeCSISequenceToTerminal((byte)'?', (byte)'1', (byte)'0', (byte)'0', (byte)'3', (byte)'h');
break;
}
if(getCharset().equals(Charset.forName("UTF-8"))) {
writeCSISequenceToTerminal((byte)'?', (byte)'1', (byte)'0', (byte)'0', (byte)'5', (byte)'h');
}
}
}
/**
* Method to test if the terminal (as far as the library knows) is in private mode.
*
* @return True if there has been a call to enterPrivateMode() but not yet exitPrivateMode()
*/
boolean isInPrivateMode() {
return inPrivateMode;
}
void reportPosition() throws IOException {
writeCSISequenceToTerminal("6n".getBytes());
}
void restoreCursorPosition() throws IOException {
writeCSISequenceToTerminal("u".getBytes());
}
void saveCursorPosition() throws IOException {
writeCSISequenceToTerminal("s".getBytes());
}
@Override
public void scrollLines(int firstLine, int lastLine, int distance) throws IOException {
final String CSI = "\033[";
// some sanity checks:
if (distance == 0) { return; }
if (firstLine < 0) { firstLine = 0; }
if (lastLine < firstLine) { return; }
StringBuilder sb = new StringBuilder();
// define range:
sb.append(CSI).append(firstLine+1)
.append(';').append(lastLine+1).append('r');
// place cursor on line to scroll away from:
int target = distance > 0 ? lastLine : firstLine;
sb.append(CSI).append(target+1).append(";1H");
// do scroll:
if (distance > 0) {
int num = Math.min( distance, lastLine - firstLine + 1);
for (int i = 0; i < num; i++) { sb.append('\n'); }
} else { // distance < 0
int num = Math.min( -distance, lastLine - firstLine + 1);
for (int i = 0; i < num; i++) { sb.append("\033M"); }
}
// reset range:
sb.append(CSI).append('r');
// off we go!
writeToTerminal(sb.toString().getBytes());
}
}