/**
* Jexer - Java Text User Interface
*
* Version: $Id$
*
* Author: Kevin Lamonte, kevin.lamonte@gmail.com
*
* License: LGPLv3 or later
*
* Copyright: This module is licensed under the GNU Lesser General
* Public License Version 3. Please see the file "COPYING" in this
* directory for more information about the GNU Lesser General Public
* License Version 3.
*
* Copyright (C) 2015 Kevin Lamonte
*
* This program 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
* 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
* http://www.gnu.org/licenses/, or write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA
*/
package jexer.io;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.LinkedList;
import jexer.TKeypress;
import jexer.bits.Color;
import jexer.event.TInputEvent;
import jexer.event.TKeypressEvent;
import jexer.event.TMouseEvent;
import jexer.event.TResizeEvent;
import jexer.session.SessionInfo;
import jexer.session.TSessionInfo;
import jexer.session.TTYSessionInfo;
import static jexer.TKeypress.*;
/**
* This class has convenience methods for emitting output to ANSI
* X3.64 / ECMA-48 type terminals e.g. xterm, linux, vt100, ansi.sys,
* etc.
*/
public class ECMA48Terminal {
/**
* The session information
*/
public SessionInfo session;
/**
* Parameters being collected. E.g. if the string is \033[1;3m, then
* params[0] will be 1 and params[1] will be 3.
*/
private ArrayList params;
/**
* params[paramI] is being appended to.
*/
private int paramI;
/**
* States in the input parser
*/
private enum ParseState {
GROUND,
ESCAPE,
ESCAPE_INTERMEDIATE,
CSI_ENTRY,
CSI_PARAM,
// CSI_INTERMEDIATE,
MOUSE
}
/**
* Current parsing state
*/
private ParseState state;
/**
* The time we entered ESCAPE. If we get a bare escape
* without a code following it, this is used to return that bare
* escape.
*/
private long escapeTime;
/**
* true if mouse1 was down. Used to report mouse1 on the release event.
*/
private boolean mouse1;
/**
* true if mouse2 was down. Used to report mouse2 on the release event.
*/
private boolean mouse2;
/**
* true if mouse3 was down. Used to report mouse3 on the release event.
*/
private boolean mouse3;
/**
* Cache the cursor visibility value so we only emit the sequence when we
* need to.
*/
private boolean cursorOn = true;
/**
* Cache the last window size to figure out if a TResizeEvent needs to be
* generated.
*/
private TResizeEvent windowResize = null;
/**
* If true, then we changed System.in and need to change it back.
*/
private boolean setRawMode;
/**
* The terminal's input. If an InputStream is not specified in the
* constructor, then this InputReader will be bound to System.in with
* UTF-8 encoding.
*/
private Reader input;
/**
* The terminal's output. If an OutputStream is not specified in the
* constructor, then this PrintWriter will be bound to System.out with
* UTF-8 encoding.
*/
private PrintWriter output;
/**
* When true, the terminal is sending non-UTF8 bytes when reporting mouse
* events.
*
* TODO: Add broken mouse detection back into the reader.
*/
private boolean brokenTerminalUTFMouse = false;
/**
* Get the output writer.
*
* @return the Writer
*/
public PrintWriter getOutput() {
return output;
}
/**
* Call 'stty cooked' to set cooked mode.
*/
private void sttyCooked() {
doStty(false);
}
/**
* Call 'stty raw' to set raw mode.
*/
private void sttyRaw() {
doStty(true);
}
/**
* Call 'stty' to set raw or cooked mode.
*
* @param mode if true, set raw mode, otherwise set cooked mode
*/
private void doStty(boolean mode) {
String [] cmdRaw = {
"/bin/sh", "-c", "stty raw < /dev/tty"
};
String [] cmdCooked = {
"/bin/sh", "-c", "stty cooked < /dev/tty"
};
try {
System.out.println("spawn stty");
Process process;
if (mode == true) {
process = Runtime.getRuntime().exec(cmdRaw);
} else {
process = Runtime.getRuntime().exec(cmdCooked);
}
BufferedReader in = new BufferedReader(new InputStreamReader(process.getInputStream(), "UTF-8"));
String line = in.readLine();
if ((line != null) && (line.length() > 0)) {
System.err.println("WEIRD?! Normal output from stty: " + line);
}
while (true) {
BufferedReader err = new BufferedReader(new InputStreamReader(process.getErrorStream(), "UTF-8"));
line = err.readLine();
if ((line != null) && (line.length() > 0)) {
System.err.println("Error output from stty: " + line);
}
try{
process.waitFor();
break;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
int rc = process.exitValue();
if (rc != 0) {
System.err.println("stty returned error code: " + rc);
}
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* Constructor sets up state for getEvent()
*
* @param input an InputStream connected to the remote user, or null for
* System.in. If System.in is used, then on non-Windows systems it will
* be put in raw mode; shutdown() will (blindly!) put System.in in cooked
* mode. input is always converted to a Reader with UTF-8 encoding.
* @param output an OutputStream connected to the remote user, or null
* for System.out. output is always converted to a Writer with UTF-8
* encoding.
*/
public ECMA48Terminal(InputStream input, OutputStream output) throws UnsupportedEncodingException {
reset();
mouse1 = false;
mouse2 = false;
mouse3 = false;
if (input == null) {
this.input = new InputStreamReader(System.in, "UTF-8");
sttyRaw();
setRawMode = true;
} else {
this.input = new InputStreamReader(input);
}
// TODO: include TelnetSocket from NIB and have it implement
// SessionInfo
if (input instanceof SessionInfo) {
session = (SessionInfo)input;
}
if (session == null) {
if (input == null) {
// Reading right off the tty
session = new TTYSessionInfo();
} else {
session = new TSessionInfo();
}
}
if (output == null) {
this.output = new PrintWriter(new OutputStreamWriter(System.out,
"UTF-8"));
} else {
this.output = new PrintWriter(new OutputStreamWriter(output,
"UTF-8"));
}
// Enable mouse reporting and metaSendsEscape
this.output.printf("%s%s", mouse(true), xtermMetaSendsEscape(true));
// Hang onto the window size
windowResize = new TResizeEvent(TResizeEvent.Type.Screen,
session.getWindowWidth(), session.getWindowHeight());
}
/**
* Restore terminal to normal state
*/
public void shutdown() {
if (setRawMode) {
sttyCooked();
setRawMode = false;
}
// Disable mouse reporting and show cursor
output.printf("%s%s%s", mouse(false), cursor(true), normal());
}
/**
* Flush output
*/
public void flush() {
output.flush();
}
/**
* Reset keyboard/mouse input parser
*/
private void reset() {
state = ParseState.GROUND;
paramI = 0;
params.clear();
params.add("");
}
/**
* Produce a control character or one of the special ones (ENTER, TAB,
* etc.)
*
* @param ch Unicode code point
* @return one KEYPRESS event, either a control character (e.g. isKey ==
* false, ch == 'A', ctrl == true), or a special key (e.g. isKey == true,
* fnKey == ESC)
*/
private TKeypressEvent controlChar(char ch) {
TKeypressEvent event = new TKeypressEvent();
// System.err.printf("controlChar: %02x\n", ch);
switch (ch) {
case '\r':
// ENTER
event.key = kbEnter;
break;
case 0x1B:
// ESC
event.key = kbEsc;
break;
case '\t':
// TAB
event.key = kbTab;
break;
default:
// Make all other control characters come back as the alphabetic
// character with the ctrl field set. So SOH would be 'A' +
// ctrl.
event.key = new TKeypress(false, 0, (char)(ch + 0x40),
false, true, false);
break;
}
return event;
}
/**
* Produce special key from CSI Pn ; Pm ; ... ~
*
* @return one KEYPRESS event representing a special key
*/
private TInputEvent csiFnKey() {
int key = 0;
int modifier = 0;
if (params.size() > 0) {
key = Integer.parseInt(params.get(0));
}
if (params.size() > 1) {
modifier = Integer.parseInt(params.get(1));
}
TKeypressEvent event = new TKeypressEvent();
switch (modifier) {
case 0:
// No modifier
switch (key) {
case 1:
event.key = kbHome;
break;
case 2:
event.key = kbIns;
break;
case 3:
event.key = kbDel;
break;
case 4:
event.key = kbEnd;
break;
case 5:
event.key = kbPgUp;
break;
case 6:
event.key = kbPgDn;
break;
case 15:
event.key = kbF5;
break;
case 17:
event.key = kbF6;
break;
case 18:
event.key = kbF7;
break;
case 19:
event.key = kbF8;
break;
case 20:
event.key = kbF9;
break;
case 21:
event.key = kbF10;
break;
case 23:
event.key = kbF11;
break;
case 24:
event.key = kbF12;
break;
default:
// Unknown
return null;
}
break;
case 2:
// Shift
switch (key) {
case 1:
event.key = kbShiftHome;
break;
case 2:
event.key = kbShiftIns;
break;
case 3:
event.key = kbShiftDel;
break;
case 4:
event.key = kbShiftEnd;
break;
case 5:
event.key = kbShiftPgUp;
break;
case 6:
event.key = kbShiftPgDn;
break;
case 15:
event.key = kbShiftF5;
break;
case 17:
event.key = kbShiftF6;
break;
case 18:
event.key = kbShiftF7;
break;
case 19:
event.key = kbShiftF8;
break;
case 20:
event.key = kbShiftF9;
break;
case 21:
event.key = kbShiftF10;
break;
case 23:
event.key = kbShiftF11;
break;
case 24:
event.key = kbShiftF12;
break;
default:
// Unknown
return null;
}
break;
case 3:
// Alt
switch (key) {
case 1:
event.key = kbAltHome;
break;
case 2:
event.key = kbAltIns;
break;
case 3:
event.key = kbAltDel;
break;
case 4:
event.key = kbAltEnd;
break;
case 5:
event.key = kbAltPgUp;
break;
case 6:
event.key = kbAltPgDn;
break;
case 15:
event.key = kbAltF5;
break;
case 17:
event.key = kbAltF6;
break;
case 18:
event.key = kbAltF7;
break;
case 19:
event.key = kbAltF8;
break;
case 20:
event.key = kbAltF9;
break;
case 21:
event.key = kbAltF10;
break;
case 23:
event.key = kbAltF11;
break;
case 24:
event.key = kbAltF12;
break;
default:
// Unknown
return null;
}
break;
case 5:
// Ctrl
switch (key) {
case 1:
event.key = kbCtrlHome;
break;
case 2:
event.key = kbCtrlIns;
break;
case 3:
event.key = kbCtrlDel;
break;
case 4:
event.key = kbCtrlEnd;
break;
case 5:
event.key = kbCtrlPgUp;
break;
case 6:
event.key = kbCtrlPgDn;
break;
case 15:
event.key = kbCtrlF5;
break;
case 17:
event.key = kbCtrlF6;
break;
case 18:
event.key = kbCtrlF7;
break;
case 19:
event.key = kbCtrlF8;
break;
case 20:
event.key = kbCtrlF9;
break;
case 21:
event.key = kbCtrlF10;
break;
case 23:
event.key = kbCtrlF11;
break;
case 24:
event.key = kbCtrlF12;
break;
default:
// Unknown
return null;
}
break;
default:
// Unknown
return null;
}
// All OK, return a keypress
return event;
}
/**
* Produce mouse events based on "Any event tracking" and UTF-8
* coordinates. See
* http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
*
* @return a MOUSE_MOTION, MOUSE_UP, or MOUSE_DOWN event
*/
private TInputEvent parseMouse() {
int buttons = params.get(0).charAt(0) - 32;
int x = params.get(0).charAt(1) - 32 - 1;
int y = params.get(0).charAt(2) - 32 - 1;
// Clamp X and Y to the physical screen coordinates.
if (x >= windowResize.width) {
x = windowResize.width - 1;
}
if (y >= windowResize.height) {
y = windowResize.height - 1;
}
TMouseEvent event = new TMouseEvent(TMouseEvent.Type.MOUSE_DOWN);
event.x = x;
event.y = y;
event.absoluteX = x;
event.absoluteY = y;
// System.err.printf("buttons: %04x\r\n", buttons);
switch (buttons) {
case 0:
event.mouse1 = true;
mouse1 = true;
break;
case 1:
event.mouse2 = true;
mouse2 = true;
break;
case 2:
event.mouse3 = true;
mouse3 = true;
break;
case 3:
// Release or Move
if (!mouse1 && !mouse2 && !mouse3) {
event.type = TMouseEvent.Type.MOUSE_MOTION;
} else {
event.type = TMouseEvent.Type.MOUSE_UP;
}
if (mouse1) {
mouse1 = false;
event.mouse1 = true;
}
if (mouse2) {
mouse2 = false;
event.mouse2 = true;
}
if (mouse3) {
mouse3 = false;
event.mouse3 = true;
}
break;
case 32:
// Dragging with mouse1 down
event.mouse1 = true;
mouse1 = true;
event.type = TMouseEvent.Type.MOUSE_MOTION;
break;
case 33:
// Dragging with mouse2 down
event.mouse2 = true;
mouse2 = true;
event.type = TMouseEvent.Type.MOUSE_MOTION;
break;
case 34:
// Dragging with mouse3 down
event.mouse3 = true;
mouse3 = true;
event.type = TMouseEvent.Type.MOUSE_MOTION;
break;
case 96:
// Dragging with mouse2 down after wheelUp
event.mouse2 = true;
mouse2 = true;
event.type = TMouseEvent.Type.MOUSE_MOTION;
break;
case 97:
// Dragging with mouse2 down after wheelDown
event.mouse2 = true;
mouse2 = true;
event.type = TMouseEvent.Type.MOUSE_MOTION;
break;
case 64:
event.mouseWheelUp = true;
break;
case 65:
event.mouseWheelDown = true;
break;
default:
// Unknown, just make it motion
event.type = TMouseEvent.Type.MOUSE_MOTION;
break;
}
return event;
}
/**
* Parses the next character of input to see if an InputEvent is
* fully here.
*
* Params:
* ch = Unicode code point
* noChar = if true, ignore ch. This is currently used to
* return a bare ESC and RESIZE events.
*
* Returns:
* list of new events (which may be empty)
*/
public List getEvents(char ch) {
return getEvents(ch, false);
}
/**
* Parses the next character of input to see if an InputEvent is
* fully here.
*
* Params:
* ch = Unicode code point
* noChar = if true, ignore ch. This is currently used to
* return a bare ESC and RESIZE events.
*
* Returns:
* list of new events (which may be empty)
*/
public List getEvents(char ch, boolean noChar) {
List events = new LinkedList();
TKeypressEvent keypress;
Date now = new Date();
// ESCDELAY type timeout
if (state == ParseState.ESCAPE) {
long escDelay = now.getTime() - escapeTime;
if (escDelay > 250) {
// After 0.25 seconds, assume a true escape character
events.add(controlChar((char)0x1B));
reset();
}
}
if (noChar == true) {
int newWidth = session.getWindowWidth();
int newHeight = session.getWindowHeight();
if ((newWidth != windowResize.width) ||
(newHeight != windowResize.height)) {
TResizeEvent event = new TResizeEvent(TResizeEvent.Type.Screen,
newWidth, newHeight);
windowResize.width = newWidth;
windowResize.height = newHeight;
events.add(event);
}
// Nothing else to do, bail out
return events;
}
// System.err.printf("state: %s ch %c\r\n", state, ch);
switch (state) {
case GROUND:
if (ch == 0x1B) {
state = ParseState.ESCAPE;
escapeTime = now.getTime();
return events;
}
if (ch <= 0x1F) {
// Control character
events.add(controlChar(ch));
reset();
return events;
}
if (ch >= 0x20) {
// Normal character
keypress = new TKeypressEvent();
keypress.key.isKey = false;
keypress.key.ch = ch;
events.add(keypress);
reset();
return events;
}
break;
case ESCAPE:
if (ch <= 0x1F) {
// ALT-Control character
keypress = controlChar(ch);
keypress.key.alt = true;
events.add(keypress);
reset();
return events;
}
if (ch == 'O') {
// This will be one of the function keys
state = ParseState.ESCAPE_INTERMEDIATE;
return events;
}
// '[' goes to CSI_ENTRY
if (ch == '[') {
state = ParseState.CSI_ENTRY;
return events;
}
// Everything else is assumed to be Alt-keystroke
keypress = new TKeypressEvent();
keypress.key.isKey = false;
keypress.key.ch = ch;
keypress.key.alt = true;
if ((ch >= 'A') && (ch <= 'Z')) {
keypress.key.shift = true;
}
events.add(keypress);
reset();
return events;
case ESCAPE_INTERMEDIATE:
if ((ch >= 'P') && (ch <= 'S')) {
// Function key
keypress = new TKeypressEvent();
keypress.key.isKey = true;
switch (ch) {
case 'P':
keypress.key.fnKey = TKeypress.F1;
break;
case 'Q':
keypress.key.fnKey = TKeypress.F2;
break;
case 'R':
keypress.key.fnKey = TKeypress.F3;
break;
case 'S':
keypress.key.fnKey = TKeypress.F4;
break;
default:
break;
}
events.add(keypress);
reset();
return events;
}
// Unknown keystroke, ignore
reset();
return events;
case CSI_ENTRY:
// Numbers - parameter values
if ((ch >= '0') && (ch <= '9')) {
params.set(paramI, params.get(paramI) + ch);
state = ParseState.CSI_PARAM;
return events;
}
// Parameter separator
if (ch == ';') {
paramI++;
params.set(paramI, "");
return events;
}
if ((ch >= 0x30) && (ch <= 0x7E)) {
switch (ch) {
case 'A':
// Up
keypress = new TKeypressEvent();
keypress.key.isKey = true;
keypress.key.fnKey = TKeypress.UP;
if (params.size() > 1) {
if (params.get(1).equals("2")) {
keypress.key.shift = true;
}
if (params.get(1).equals("5")) {
keypress.key.ctrl = true;
}
if (params.get(1).equals("3")) {
keypress.key.alt = true;
}
}
events.add(keypress);
reset();
return events;
case 'B':
// Down
keypress = new TKeypressEvent();
keypress.key.isKey = true;
keypress.key.fnKey = TKeypress.DOWN;
if (params.size() > 1) {
if (params.get(1).equals("2")) {
keypress.key.shift = true;
}
if (params.get(1).equals("5")) {
keypress.key.ctrl = true;
}
if (params.get(1).equals("3")) {
keypress.key.alt = true;
}
}
events.add(keypress);
reset();
return events;
case 'C':
// Right
keypress = new TKeypressEvent();
keypress.key.isKey = true;
keypress.key.fnKey = TKeypress.RIGHT;
if (params.size() > 1) {
if (params.get(1).equals("2")) {
keypress.key.shift = true;
}
if (params.get(1).equals("5")) {
keypress.key.ctrl = true;
}
if (params.get(1).equals("3")) {
keypress.key.alt = true;
}
}
events.add(keypress);
reset();
return events;
case 'D':
// Left
keypress = new TKeypressEvent();
keypress.key.isKey = true;
keypress.key.fnKey = TKeypress.LEFT;
if (params.size() > 1) {
if (params.get(1).equals("2")) {
keypress.key.shift = true;
}
if (params.get(1).equals("5")) {
keypress.key.ctrl = true;
}
if (params.get(1).equals("3")) {
keypress.key.alt = true;
}
}
events.add(keypress);
reset();
return events;
case 'H':
// Home
keypress = new TKeypressEvent();
keypress.key.isKey = true;
keypress.key.fnKey = TKeypress.HOME;
events.add(keypress);
reset();
return events;
case 'F':
// End
keypress = new TKeypressEvent();
keypress.key.isKey = true;
keypress.key.fnKey = TKeypress.END;
events.add(keypress);
reset();
return events;
case 'Z':
// CBT - Cursor backward X tab stops (default 1)
keypress = new TKeypressEvent();
keypress.key.isKey = true;
keypress.key.fnKey = TKeypress.BTAB;
events.add(keypress);
reset();
return events;
case 'M':
// Mouse position
state = ParseState.MOUSE;
return events;
default:
break;
}
}
// Unknown keystroke, ignore
reset();
return events;
case CSI_PARAM:
// Numbers - parameter values
if ((ch >= '0') && (ch <= '9')) {
params.set(paramI, params.get(paramI) + ch);
state = ParseState.CSI_PARAM;
return events;
}
// Parameter separator
if (ch == ';') {
paramI++;
params.set(paramI, "");
return events;
}
if (ch == '~') {
events.add(csiFnKey());
reset();
return events;
}
if ((ch >= 0x30) && (ch <= 0x7E)) {
switch (ch) {
case 'A':
// Up
keypress = new TKeypressEvent();
keypress.key.isKey = true;
keypress.key.fnKey = TKeypress.UP;
if (params.size() > 1) {
if (params.get(1).equals("2")) {
keypress.key.shift = true;
}
if (params.get(1).equals("5")) {
keypress.key.ctrl = true;
}
if (params.get(1).equals("3")) {
keypress.key.alt = true;
}
}
events.add(keypress);
reset();
return events;
case 'B':
// Down
keypress = new TKeypressEvent();
keypress.key.isKey = true;
keypress.key.fnKey = TKeypress.DOWN;
if (params.size() > 1) {
if (params.get(1).equals("2")) {
keypress.key.shift = true;
}
if (params.get(1).equals("5")) {
keypress.key.ctrl = true;
}
if (params.get(1).equals("3")) {
keypress.key.alt = true;
}
}
events.add(keypress);
reset();
return events;
case 'C':
// Right
keypress = new TKeypressEvent();
keypress.key.isKey = true;
keypress.key.fnKey = TKeypress.RIGHT;
if (params.size() > 1) {
if (params.get(1).equals("2")) {
keypress.key.shift = true;
}
if (params.get(1).equals("5")) {
keypress.key.ctrl = true;
}
if (params.get(1).equals("3")) {
keypress.key.alt = true;
}
}
events.add(keypress);
reset();
return events;
case 'D':
// Left
keypress = new TKeypressEvent();
keypress.key.isKey = true;
keypress.key.fnKey = TKeypress.LEFT;
if (params.size() > 1) {
if (params.get(1).equals("2")) {
keypress.key.shift = true;
}
if (params.get(1).equals("5")) {
keypress.key.ctrl = true;
}
if (params.get(1).equals("3")) {
keypress.key.alt = true;
}
}
events.add(keypress);
reset();
return events;
default:
break;
}
}
// Unknown keystroke, ignore
reset();
return events;
case MOUSE:
params.set(0, params.get(paramI) + ch);
if (params.get(0).length() == 3) {
// We have enough to generate a mouse event
events.add(parseMouse());
reset();
}
return events;
default:
break;
}
// This "should" be impossible to reach
return events;
}
/**
* Tell (u)xterm that we want alt- keystrokes to send escape +
* character rather than set the 8th bit. Anyone who wants UTF8
* should want this enabled.
*
* Params:
* on = if true, enable metaSendsEscape
*
* Returns:
* the string to emit to xterm
*/
static public String xtermMetaSendsEscape(boolean on) {
if (on) {
return "\033[?1036h\033[?1034l";
}
return "\033[?1036l";
}
/**
* Convert a list of SGR parameters into a full escape sequence.
* This also eliminates a trailing ';' which would otherwise reset
* everything to white-on-black not-bold.
*
* Params:
* str = string of parameters, e.g. "31;1;"
*
* Returns:
* the string to emit to an ANSI / ECMA-style terminal, e.g. "\033[31;1m"
*/
static public String addHeaderSGR(String str) {
if (str.length() > 0) {
// Nix any trailing ';' because that resets all attributes
while (str.endsWith(":")) {
str = str.substring(0, str.length() - 1);
}
}
return "\033[" + str + "m";
}
/**
* Create a SGR parameter sequence for a single color change.
*
* Params:
* color = one of the Color.WHITE, Color.BLUE, etc. constants
* foreground = if true, this is a foreground color
*
* Returns:
* the string to emit to an ANSI / ECMA-style terminal, e.g. "\033[42m"
*/
static public String color(Color color, boolean foreground) {
return color(color, foreground, true);
}
/**
* Create a SGR parameter sequence for a single color change.
*
* Params:
* color = one of the Color.WHITE, Color.BLUE, etc. constants
* foreground = if true, this is a foreground color
* header = if true, make the full header, otherwise just emit
* the color parameter e.g. "42;"
*
* Returns:
* the string to emit to an ANSI / ECMA-style terminal, e.g. "\033[42m"
*/
static public String color(Color color, boolean foreground,
boolean header) {
int ecmaColor = color.value;
// Convert Color.* values to SGR numerics
if (foreground == true) {
ecmaColor += 30;
} else {
ecmaColor += 40;
}
if (header) {
return String.format("\033[%dm", ecmaColor);
} else {
return String.format("%d;", ecmaColor);
}
}
/**
* Create a SGR parameter sequence for both foreground and
* background color change.
*
* Params:
* foreColor = one of the Color.WHITE, Color.BLUE, etc. constants
* backColor = one of the Color.WHITE, Color.BLUE, etc. constants
*
* Returns:
* the string to emit to an ANSI / ECMA-style terminal, e.g. "\033[31;42m"
*/
static public String color(Color foreColor, Color backColor) {
return color(foreColor, backColor, true);
}
/**
* Create a SGR parameter sequence for both foreground and
* background color change.
*
* Params:
* foreColor = one of the Color.WHITE, Color.BLUE, etc. constants
* backColor = one of the Color.WHITE, Color.BLUE, etc. constants
* header = if true, make the full header, otherwise just emit
* the color parameter e.g. "31;42;"
*
* Returns:
* the string to emit to an ANSI / ECMA-style terminal, e.g. "\033[31;42m"
*/
static public String color(Color foreColor, Color backColor,
boolean header) {
int ecmaForeColor = foreColor.value;
int ecmaBackColor = backColor.value;
// Convert Color.* values to SGR numerics
ecmaBackColor += 40;
ecmaForeColor += 30;
if (header) {
return String.format("\033[%d;%dm", ecmaForeColor, ecmaBackColor);
} else {
return String.format("%d;%d;", ecmaForeColor, ecmaBackColor);
}
}
/**
* Create a SGR parameter sequence for foreground, background, and
* several attributes. This sequence first resets all attributes
* to default, then sets attributes as per the parameters.
*
* Params:
* foreColor = one of the Color.WHITE, Color.BLUE, etc. constants
* backColor = one of the Color.WHITE, Color.BLUE, etc. constants
* bold = if true, set bold
* reverse = if true, set reverse
* blink = if true, set blink
* underline = if true, set underline
*
* Returns:
* the string to emit to an ANSI / ECMA-style terminal, e.g. "\033[0;1;31;42m"
*/
static public String color(Color foreColor, Color backColor, boolean bold,
boolean reverse, boolean blink, boolean underline) {
int ecmaForeColor = foreColor.value;
int ecmaBackColor = backColor.value;
// Convert Color.* values to SGR numerics
ecmaBackColor += 40;
ecmaForeColor += 30;
StringBuilder sb = new StringBuilder();
if ( bold && reverse && blink && !underline ) {
sb.append("\033[0;1;7;5;");
} else if ( bold && reverse && !blink && !underline ) {
sb.append("\033[0;1;7;");
} else if ( !bold && reverse && blink && !underline ) {
sb.append("\033[0;7;5;");
} else if ( bold && !reverse && blink && !underline ) {
sb.append("\033[0;1;5;");
} else if ( bold && !reverse && !blink && !underline ) {
sb.append("\033[0;1;");
} else if ( !bold && reverse && !blink && !underline ) {
sb.append("\033[0;7;");
} else if ( !bold && !reverse && blink && !underline) {
sb.append("\033[0;5;");
} else if ( bold && reverse && blink && underline ) {
sb.append("\033[0;1;7;5;4;");
} else if ( bold && reverse && !blink && underline ) {
sb.append("\033[0;1;7;4;");
} else if ( !bold && reverse && blink && underline ) {
sb.append("\033[0;7;5;4;");
} else if ( bold && !reverse && blink && underline ) {
sb.append("\033[0;1;5;4;");
} else if ( bold && !reverse && !blink && underline ) {
sb.append("\033[0;1;4;");
} else if ( !bold && reverse && !blink && underline ) {
sb.append("\033[0;7;4;");
} else if ( !bold && !reverse && blink && underline) {
sb.append("\033[0;5;4;");
} else if ( !bold && !reverse && !blink && underline) {
sb.append("\033[0;4;");
} else {
assert(!bold && !reverse && !blink && !underline);
sb.append("\033[0;");
}
sb.append(String.format("%d;%dm", ecmaForeColor, ecmaBackColor));
return sb.toString();
}
/**
* Create a SGR parameter sequence for enabling reverse color.
*
* Params:
* on = if true, turn on reverse
*
* Returns:
* the string to emit to an ANSI / ECMA-style terminal, e.g. "\033[7m"
*/
static public String reverse(boolean on) {
if (on) {
return "\033[7m";
}
return "\033[27m";
}
/**
* Create a SGR parameter sequence to reset to defaults.
*
* Returns:
* the string to emit to an ANSI / ECMA-style terminal, e.g. "\033[0m"
*/
static public String normal() {
return normal(true);
}
/**
* Create a SGR parameter sequence to reset to defaults.
*
* Params:
* header = if true, make the full header, otherwise just emit
* the bare parameter e.g. "0;"
*
* Returns:
* the string to emit to an ANSI / ECMA-style terminal, e.g. "\033[0m"
*/
static public String normal(boolean header) {
if (header) {
return "\033[0;37;40m";
}
return "0;37;40";
}
/**
* Create a SGR parameter sequence for enabling boldface.
*
* Params:
* on = if true, turn on bold
*
* Returns:
* the string to emit to an ANSI / ECMA-style terminal, e.g. "\033[1m"
*/
static public String bold(boolean on) {
return bold(on, true);
}
/**
* Create a SGR parameter sequence for enabling boldface.
*
* Params:
* on = if true, turn on bold
* header = if true, make the full header, otherwise just emit
* the bare parameter e.g. "1;"
*
* Returns:
* the string to emit to an ANSI / ECMA-style terminal, e.g. "\033[1m"
*/
static public String bold(boolean on, boolean header) {
if (header) {
if (on) {
return "\033[1m";
}
return "\033[22m";
}
if (on) {
return "1;";
}
return "22;";
}
/**
* Create a SGR parameter sequence for enabling blinking text.
*
* Params:
* on = if true, turn on blink
*
* Returns:
* the string to emit to an ANSI / ECMA-style terminal, e.g. "\033[5m"
*/
static public String blink(boolean on) {
return blink(on, true);
}
/**
* Create a SGR parameter sequence for enabling blinking text.
*
* Params:
* on = if true, turn on blink
* header = if true, make the full header, otherwise just emit
* the bare parameter e.g. "5;"
*
* Returns:
* the string to emit to an ANSI / ECMA-style terminal, e.g. "\033[5m"
*/
static public String blink(boolean on, boolean header) {
if (header) {
if (on) {
return "\033[5m";
}
return "\033[25m";
}
if (on) {
return "5;";
}
return "25;";
}
/**
* Create a SGR parameter sequence for enabling underline /
* underscored text.
*
* Params:
* on = if true, turn on underline
*
* Returns:
* the string to emit to an ANSI / ECMA-style terminal, e.g. "\033[4m"
*/
static public String underline(boolean on) {
if (on) {
return "\033[4m";
}
return "\033[24m";
}
/**
* Create a SGR parameter sequence for enabling the visible cursor.
*
* Params:
* on = if true, turn on cursor
*
* Returns:
* the string to emit to an ANSI / ECMA-style terminal
*/
public String cursor(boolean on) {
if (on && (cursorOn == false)) {
cursorOn = true;
return "\033[?25h";
}
if (!on && (cursorOn == true)) {
cursorOn = false;
return "\033[?25l";
}
return "";
}
/**
* Clear the entire screen. Because some terminals use back-color-erase,
* set the color to white-on-black beforehand.
*
* Returns:
* the string to emit to an ANSI / ECMA-style terminal
*/
static public String clearAll() {
return "\033[0;37;40m\033[2J";
}
/**
* Clear the line from the cursor (inclusive) to the end of the screen.
* Because some terminals use back-color-erase, set the color to
* white-on-black beforehand.
*
* Returns:
* the string to emit to an ANSI / ECMA-style terminal
*/
static public String clearRemainingLine() {
return "\033[0;37;40m\033[K";
}
/**
* Clear the line up the cursor (inclusive). Because some terminals use
* back-color-erase, set the color to white-on-black beforehand.
*
* Returns:
* the string to emit to an ANSI / ECMA-style terminal
*/
static public String clearPreceedingLine() {
return "\033[0;37;40m\033[1K";
}
/**
* Clear the line. Because some terminals use back-color-erase, set the
* color to white-on-black beforehand.
*
* Returns:
* the string to emit to an ANSI / ECMA-style terminal
*/
static public String clearLine() {
return "\033[0;37;40m\033[2K";
}
/**
* Move the cursor to the top-left corner.
*
* Returns:
* the string to emit to an ANSI / ECMA-style terminal
*/
static public String home() {
return "\033[H";
}
/**
* Move the cursor to (x, y).
*
* Params:
* x = column coordinate. 0 is the left-most column.
* y = row coordinate. 0 is the top-most row.
*
* Returns:
* the string to emit to an ANSI / ECMA-style terminal
*/
static public String gotoXY(int x, int y) {
return String.format("\033[%d;%dH", y + 1, x + 1);
}
/**
* Tell (u)xterm that we want to receive mouse events based on
* "Any event tracking" and UTF-8 coordinates. See
* http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
*
* Finally, this sets the alternate screen buffer.
*
* Params:
* on = if true, enable mouse report
*
* Returns:
* the string to emit to xterm
*/
static public String mouse(boolean on) {
if (on) {
return "\033[?1003;1005h\033[?1049h";
}
return "\033[?1003;1005l\033[?1049l";
}
}